{
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved",
"items": [
{
"originId": "tag:blogger.com,1999:blog-8954608646904080796.post-6562845574025332924",
"fingerprint": "57441f5a",
"thumbnail": [
{
"url": "https://1.bp.blogspot.com/-nSjfvVEYsOE/XZ6cEycVw3I/AAAAAAAADTI/UMdV1Seh7R8c0GdV2RgwjuAoJLW47it1gCLcBGAsYHQ/s72-c/074.jpg",
"width": 72,
"height": 72
}
],
"id": "v0v+7Ya8tssIZvd3/pcnFRr3HwvY/5YK3FGc2t65c0Y=_16db6126dbf:685a:d4506071",
"updated": 1570675865985,
"author": "Edward Feser",
"alternate": [
{
"href": "http://edwardfeser.blogspot.com/2019/10/transubstantiation-and-hylemorphism.html",
"type": "text/html"
}
],
"crawled": 1570717724095,
"title": "Transubstantiation and hylemorphism",
"published": 1570675860000,
"origin": {
"streamId": "feed/http://edwardfeser.blogspot.com/feeds/posts/default",
"htmlUrl": "http://edwardfeser.blogspot.com/",
"title": "Edward Feser"
},
"content": {
"direction": "ltr",
"content": "
At WWDC this year, Apple announced a coordinated effort between Xcode 11 and iOS 13 to bring new insights to developers about how their apps are performing in the field.
" }, "alternate": [ { "href": "http://feedproxy.google.com/~r/NSHipster/~3/o2-j6xKjBrA/", "type": "text/html" } ], "canonical": [ { "href": "https://nshipster.com/metrickit/", "type": "text/html" } ], "crawled": 1571854910253, "title": "MetricKit", "published": 1571641200000, "origin": { "streamId": "feed/http://feeds.feedburner.com/NSHipster", "htmlUrl": "https://nshipster.com/", "title": "NSHipster" }, "content": { "direction": "ltr", "content": "As an undergraduate student,\nI had a radio show called\n\u201cGoodbye, Blue Monday\u201d\n(I was really into Vonnegut at the time).\nIt was nothing glamorous \u2014\njust a weekly, 2-hour slot at the end of the night\nbefore the station switched into automation.
\nIf you happened to be driving through the hills of Pittsburgh, Pennsylvania\nlate at night with your radio tuned to\nWRCT 88.3,\nyou\u2019d have heard an eclectic mix of\nContemporary Classical,\nAcid Jazz,\nItalian Disco, and\nBebop.\nThat, and the stilting, dulcet baritone of\na college kid doing his best impersonation of\nTony Mowod.
\nSitting there in the booth,\nwaiting for tracks to play out before launching into an\nFCC-mandated\nPSA\nor on-the-hour\nstation identification,\nI\u2019d wonder:\nIs anyone out there listening?\nAnd if they were, did they like it?\nI could\u2019ve been broadcasting static the whole time and been none the wiser.
\nThe same thoughts come to mind whenever I submit a build to App Store Connect\u2026\nbut then I\u2019ll remember that, unlike radio,\nyou can actually know these things!\nAnd the latest improvements in Xcode 11 make it easier than ever\nto get an idea of how your apps are performing in the field.
\nWe\u2019ll cover everything you need to know in this week\u2019s NSHipster article.\nSo as they say on the radio:\n\u201cDon\u2019t touch that dial (it\u2019s got jam on it)\u201d.
\nMetricKit is a new framework in iOS 13\nfor collecting and processing battery and performance metrics.\nIt was announced at WWDC this year\nalong with XCTest Metrics and the Xcode Metrics Organizer\nas part of a coordinated effort to bring new insights to developers\nabout how their apps are performing in the field.
\nApple automatically collects metrics from apps installed on the App Store.\nYou can view them in Xcode 11\nby opening the Organizer (\u2325\u2318\u21e7O)\nand selecting the new Metrics tab.
\nMetricKit complement Xcode Organizer Metrics by providing a programmatic way to\nreceive daily information about how your app is performing in the field.\nWith this information,\nyou can collect, aggregate, and analyze on your own in greater detail\nthan you can through Xcode.
\nMetrics can help uncover issues you might not have seen while testing locally,\nand allow you to track changes across different versions of your app.\nFor this initial release,\nApple has focused on the two metrics that matter most to users:\nbattery usage and performance.
\nBattery life depends on a lot of different factors.\nPhysical aspects like the age of the device and\nthe number of charge cycles are determinative,\nbut the way your phone is used matters, too.\nThings like CPU usage,\nthe brightness of the display and the colors on the screen,\nand how often radios are used to fetch data or get your current location \u2014\nall of these can have a big impact.\nBut the main thing to keep in mind is that\nusers care a lot about battery life.
\nAside from how good the camera is,\nthe amount of time between charges\nis the deciding factor when someone buys a new phone these days.\nSo when their new, expensive phone doesn\u2019t make it through the day,\nthey\u2019re going to be pretty unhappy.
\nUntil recently,\nApple\u2019s taken most of the heat on battery issues.\nBut since iOS 12 and its new\nBattery Usage screen in Settings,\nusers now have a way to tell when their favorite app is to blame.\nFortunately,\nwith iOS 13 you now have everything you need to make sure\nyour app doesn\u2019t run afoul of reasonable energy usage.
\nPerformance is another key factor in the overall user experience.\nNormally, we might look to stats like\nprocessor clock speed or frame rate\nas a measure of performance.\nBut instead,\nApple\u2019s focusing on less abstract and more actionable metrics:
\nFrom the perspective of an API consumer,\nit\u2019s hard to imagine how MetricKit could be easier to incorporate.\nAll you need is for some part of your app to serve as\na metric subscriber\n(an obvious choice is your App
),\nand for it to be added to the shared MXMetric
:
import UIKit\n import MetricKit \n @UIApplicationMain \n class AppDelegate : UIResponder, UIApplicationDelegate {\n func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions : [UIApplication.LaunchOptionsKey : Any]?) -> Bool {\n MXMetricManager .shared.add(self)\n return true\n }\n func applicationWillTerminate (_ application: UIApplication) {\n MXMetricManager .shared.remove(self)\n }\n }\n extension AppDelegate : MXMetricManagerSubscriber {\n func didReceive (_ payloads: [MXMetricPayload ]) {\n ...\n }\n }\n
\niOS automatically collects samples while your app is being used,\nand once per day (every 24 hours),\nit\u2019ll send an aggregated report with those metrics.
\nTo verify that your MXMetric
\nis having its delegate method called as expected,\nselect Simulate MetricKit Payloads from the Debug menu\nwhile Xcode is running your app.
In addition to the baseline statistics collected for you,\nyou can use the\nmx
function\nto collect metrics around the most important parts of your code.\nThis signpost-backed API\ncaptures CPU time, memory, and writes to disk.
For example,\nif part of your app did post-processing on audio streams,\nyou might annotate those regions with metric signposts\nto determine the energy and performance impact of that work:
\nlet audioLogHandle = MXMetricManager .makeLogHandle (category: "Audio")\n func processAudioStream () {\n mxSignpost (.begin, log: audioLogHandle , name: "ProcessAudioStream" )\n ...\n mxSignpost (.end, log: audioLogHandle , name: "ProcessAudioStream" )\n }\n
\nNow that you have this information,\nwhat do you do with it?\nHow do we fill that ...
placeholder in our implementation of did
?
You could pass that along to some paid analytics or crash reporting service,\nbut where\u2019s the fun in that?\nLet\u2019s build our own web service to collect these for further analysis:
\nThe MXMetric
objects received by metrics manager subscribers\nhave a convenient\njson
method\nthat generates something like this:
{\n"locationActivityMetrics" : {\n"cumulativeBestAccuracyForNavigationTime" : "20 sec",\n"cumulativeBestAccuracyTime" : "30 sec",\n"cumulativeHundredMetersAccuracyTime" : "30 sec",\n"cumulativeNearestTenMetersAccuracyTime" : "30 sec",\n"cumulativeKilometerAccuracyTime" : "20 sec",\n"cumulativeThreeKilometersAccuracyTime" : "20 sec"\n},\n"cellularConditionMetrics" : {\n"cellConditionTime" : {\n"histogramNumBuckets" : 3,\n"histogramValue" : {\n"0": {\n"bucketCount" : 20,\n"bucketStart" : "1 bars",\n"bucketEnd" : "1 bars"\n},\n"1": {\n"bucketCount" : 30,\n"bucketStart" : "2 bars",\n"bucketEnd" : "2 bars"\n},\n"2": {\n"bucketCount" : 50,\n"bucketStart" : "3 bars",\n"bucketEnd" : "3 bars"\n}\n}\n}\n},\n"metaData" : {\n"appBuildVersion" : "0",\n"osVersion" : "iPhone OS 13.1.3 (17A878)" ,\n"regionFormat" : "US",\n"deviceType" : "iPhone9,2" \n},\n"gpuMetrics" : {\n"cumulativeGPUTime" : "20 sec"\n},\n"memoryMetrics" : {\n"peakMemoryUsage" : "200,000 kB" ,\n"averageSuspendedMemory" : {\n"averageValue" : "100,000 kB" ,\n"standardDeviation" : 0,\n"sampleCount" : 500\n}\n},\n"signpostMetrics" : [\n{\n"signpostIntervalData" : {\n"histogrammedSignpostDurations" : {\n"histogramNumBuckets" : 3,\n"histogramValue" : {\n"0": {\n"bucketCount" : 50,\n"bucketStart" : "0 ms",\n"bucketEnd" : "100 ms"\n},\n"1": {\n"bucketCount" : 60,\n"bucketStart" : "100 ms",\n"bucketEnd" : "400 ms"\n},\n"2": {\n"bucketCount" : 30,\n"bucketStart" : "400 ms",\n"bucketEnd" : "700 ms"\n}\n}\n},\n"signpostCumulativeCPUTime" : "30,000 ms",\n"signpostAverageMemory" : "100,000 kB" ,\n"signpostCumulativeLogicalWrites" : "600 kB" \n},\n"signpostCategory" : "TestSignpostCategory1" ,\n"signpostName" : "TestSignpostName1" ,\n"totalSignpostCount" : 30\n},\n{\n"signpostIntervalData" : {\n"histogrammedSignpostDurations" : {\n"histogramNumBuckets" : 3,\n"histogramValue" : {\n"0": {\n"bucketCount" : 60,\n"bucketStart" : "0 ms",\n"bucketEnd" : "200 ms"\n},\n"1": {\n"bucketCount" : 70,\n"bucketStart" : "201 ms",\n"bucketEnd" : "300 ms"\n},\n"2": {\n"bucketCount" : 80,\n"bucketStart" : "301 ms",\n"bucketEnd" : "500 ms"\n}\n}\n},\n"signpostCumulativeCPUTime" : "50,000 ms",\n"signpostAverageMemory" : "60,000 kB" ,\n"signpostCumulativeLogicalWrites" : "700 kB" \n},\n"signpostCategory" : "TestSignpostCategory2" ,\n"signpostName" : "TestSignpostName2" ,\n"totalSignpostCount" : 40\n}\n],\n"displayMetrics" : {\n"averagePixelLuminance" : {\n"averageValue" : "50 apl",\n"standardDeviation" : 0,\n"sampleCount" : 500\n}\n},\n"cpuMetrics" : {\n"cumulativeCPUTime" : "100 sec"\n},\n"networkTransferMetrics" : {\n"cumulativeCellularDownload" : "80,000 kB" ,\n"cumulativeWifiDownload" : "60,000 kB" ,\n"cumulativeCellularUpload" : "70,000 kB" ,\n"cumulativeWifiUpload" : "50,000 kB" \n},\n"diskIOMetrics" : {\n"cumulativeLogicalWrites" : "1,300 kB" \n},\n"applicationLaunchMetrics" : {\n"histogrammedTimeToFirstDrawKey" : {\n"histogramNumBuckets" : 3,\n"histogramValue" : {\n"0": {\n"bucketCount" : 50,\n"bucketStart" : "1,000 ms",\n"bucketEnd" : "1,010 ms"\n},\n"1": {\n"bucketCount" : 60,\n"bucketStart" : "2,000 ms",\n"bucketEnd" : "2,010 ms"\n},\n"2": {\n"bucketCount" : 30,\n"bucketStart" : "3,000 ms",\n"bucketEnd" : "3,010 ms"\n}\n}\n},\n"histogrammedResumeTime" : {\n"histogramNumBuckets" : 3,\n"histogramValue" : {\n"0": {\n"bucketCount" : 60,\n"bucketStart" : "200 ms",\n"bucketEnd" : "210 ms"\n},\n"1": {\n"bucketCount" : 70,\n"bucketStart" : "300 ms",\n"bucketEnd" : "310 ms"\n},\n"2": {\n"bucketCount" : 80,\n"bucketStart" : "500 ms",\n"bucketEnd" : "510 ms"\n}\n}\n}\n},\n"applicationTimeMetrics" : {\n"cumulativeForegroundTime" : "700 sec",\n"cumulativeBackgroundTime" : "40 sec",\n"cumulativeBackgroundAudioTime" : "30 sec",\n"cumulativeBackgroundLocationTime" : "30 sec"\n},\n"timeStampEnd" : "2019-10-22 06:59:00 +0000",\n"applicationResponsivenessMetrics" : {\n"histogrammedAppHangTime" : {\n"histogramNumBuckets" : 3,\n"histogramValue" : {\n"0": {\n"bucketCount" : 50,\n"bucketStart" : "0 ms",\n"bucketEnd" : "100 ms"\n},\n"1": {\n"bucketCount" : 60,\n"bucketStart" : "100 ms",\n"bucketEnd" : "400 ms"\n},\n"2": {\n"bucketCount" : 30,\n"bucketStart" : "400 ms",\n"bucketEnd" : "700 ms"\n}\n}\n}\n},\n"appVersion" : "1.0.0",\n"timeStampBegin" : "2019-10-21 07:00:00 +0000"\n}\n
\nAs you can see,\nthere\u2019s a lot baked into this representation.\nDefining a schema for all of this information would be a lot of work,\nand there\u2019s no guarantee that this won\u2019t change in the future.\nSo instead,\nlet\u2019s embrace the NoSQL paradigm\n(albeit responsibly, using Postgres)\nby storing payloads in a JSONB
column:
CREATE TABLE IF NOT EXISTS metrics (\n id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n payload JSONB NOT NULL\n );\n
\nSo easy!
\nWe can extract individual fields from payloads\nusing JSON operators\nlike so:
\nSELECT (payload -> 'applicationTimeMetrics' \n ->> 'cumulativeForegroundTime' )::INTERVAL\n FROM metrics;\n -- interval\n -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n -- @ 11 mins 40 secs\n -- (1 row)\n
\n\nJSON operators in PostgreSQL can be cumbersome to work with \u2014\nespecially for more complex queries.\nOne way to help with that is to create a view\n(materialized or otherwise)\nto project the most important information to you\nin the most convenient representation:
\nCREATE VIEW key_performance_indicators AS\n SELECT\n id,\n (payload -> 'appVersion' ) AS app_version,\n (payload -> 'metaData' ->> 'deviceType' ) AS device_type,\n (payload -> 'metaData' ->> 'regionFormat' ) AS region,\n (payload -> 'applicationTimeMetrics' \n ->> 'cumulativeForegroundTime' \n )::INTERVAL AS cumulative_foreground_time,\n parse_byte_count(\n payload -> 'memoryMetrics' \n ->> 'peakMemoryUsage' \n ) AS peak_memory_usage_bytes\n FROM metrics;\n
\nWith views,\nyou can perform\naggregate queries\nover all of your metrics JSON payloads\nwith the convenience of a schema-backed relational database:
\nSELECT avg(cumulative_foreground_time)\n FROM key_performance_indicators;\n -- avg\n -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n -- @ 9 mins 41 secs\n SELECT app_version, percentile_disc(0.5)\n WITHIN GROUP (ORDER BY peak_memory_usage_bytes)\n AS median\n FROM key_performance_indicators\n GROUP BY app_version;\n -- app_version \u2502 median\n -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n -- "1.0.1" \u2502 192500000\n -- "1.0.0" \u2502 204800000\n
\n\nIn this example,\nmost of the heavy lifting is delegated to Postgres,\nmaking the server-side implementation rather boring.\nFor completeness,\nhere are some reference implementations in\nRuby (Sinatra) and JavaScript (Express):
\nrequire 'sinatra/base'\n require 'pg'\n require 'sequel'\n class App < Sinatra::Base\n configure do\n DB = Sequel.connect(ENV['DATABASE_URL'])\n end\n post '/collect' do\n DB[:metrics].insert(payload: request.body.read)\n status 204\n end\n end\n
\nimport express from 'express';\n import { Pool } from 'pg';\n const db = new Pool(\n connectionString : process.env.DATABASE_URL,\n ssl: process.env.NODE_ENV === 'production'\n );\n const app = express();\n app.post('/collect', (request, response) => {\n db.query('INSERT INTO metrics (payload) VALUES ($1)', [request.body], (error, results) => {\n if (error) {\n throw error;\n }\n response.status(204);\n })\n });\n app.listen(process.env.PORT || 5000)\n
\nNow that we have everything set up,\nthe final step is to implement\nthe required MXMetric
delegate method did
\nto pass that information along to our web service:
extension AppDelegate : MXMetricManagerSubscriber {\n func didReceive (_ payloads: [MXMetricPayload ]) {\n for payload in payloads {\n let url = URL(string: "https://example.com/collect")!\n var request = URLRequest(url: url)\n request.httpMethod = "POST"\n request.httpBody = payload.jsonRepresentation ()\n let task = URLSession.shared.dataTask (with: request)\n task.priority = URLSessionTask .lowPriority \n task.resume()\n }\n }\n }\n
\nWhen you create something and put it out into the world,\nyou lose your direct connection to it.\nThat\u2019s as true for apps as it is for college radio shows.\nShort of user research studies or\ninvasive ad-tech,\nthe truth is that\nwe rarely have any clue about how people are using our software.
\n\nMetrics offer a convenient way to at least make sure that\nthings aren\u2019t too slow or too draining.\nAnd though they provide but a glimpse in the aggregate\nof how our apps are being enjoyed,\nit\u2019s just enough to help us honor both our creation and our audience\nwith a great user experience.
\n\nThe main things in this release are 1) enhanced performance and 2) importing subscriptions from NetNewsWire 3 (since it won\u2018t run on Catalina).
\nThere are also a bunch of bug fixes \u2014 including a fix for the space bar behavior on Catalina \u2014 and there\u2019s a new feature: you can type the s
key to star and unstar an article.
For more details, read the change notes on the NetNewsWire blog.
" }, "alternate": [ { "href": "https://inessential.com/2019/10/22/netnewswire_5_0_3_for_mac_released", "type": "text/html" } ], "crawled": 1571778591946, "title": "NetNewsWire 5.0.3 for Mac Released", "published": 1571775505000, "origin": { "streamId": "feed/http://ranchero.com/xml/rss.xml", "htmlUrl": "https://inessential.com/", "title": "inessential.com" }, "visual": { "url": "http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg", "width": 620, "height": 340, "contentType": "image/jpg" }, "unread": false, "readTime": 5621, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8", "label": "THree" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" } ], "actionTimestamp": 1572500226675 }, { "id": "AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16db2add61b:afd:a4acdac", "originId": "58495.pz39s0 at https://www.imore.com", "fingerprint": "8fe463a6", "content": { "content": "An Apple Support rep apparently said, "I do not know how this could of happened."
\n\nWhen Apple Card debuted, one of its biggest draws was Apple's focus on security. On Apple's website, it says, "It's hard to steal a credit card number when you can't see it." But that's apparently what happened to one Apple Card user who reached out to 9to5Mac, claiming they were the victim of fraud.
\nThe Apple Card user said they reached out to Apple Support and received this response:
\n\n\nI do not know how this could of happened. It's very rare for your card to be in two places at one time. Since our physical cards have no number on it, it's very hard for someone to copy it.
\n
The Apple Card user confirmed the fraudulent charge after receiving an alert on his iPhone. The tricky thing is the purchase was apparently labeled as being nearby, but clicking on the map revealed it was hours away, 9to5Mac explained.
\nOn Apple's website, the company highlights the fact that the Apple Card doesn't have any numbers on it. "Not even a CVV. So that's one less thing to worry about when you hand over your card at a restaurant or store." But that doesn't guarantee it can't be stolen.
\n9to5Mac speculates that the Apple Card user may have been the victim of skimming, which can potentially affect all credit cards and debit cards. It's a reminder to be extra vigilant when swiping your card at a gas station or ATM. Better yet, use Apple Pay when possible.
\n\n\nMost Xcode users quickly become familiar with the basics of the Find Navigator panel.
\nWith it, you can find text, regular expressions, and perform search-and-replace, whether matching or ignoring case. But that\u2019s just scratching the surface of the Find Navigator.
\nI thought I\u2019d drop a few words today about search scopes. Controlled from the bottom left, \u00a0under the search field, you can create narrowed searches. This enables you to, for example, search only in Swift files or exclude files containing the word Test.
\nTo get started, click the icon (two lines with three squares on a line between them) and then New Scope (the plus icon). Here, you can name the scope, limit the search extent, and add criteria for exactly which files should be included or not.
\nThe logic is straightforward. You choose where to look (the project, a folder, or through the entire SDK), and whether to include all conditions or some conditions:
\nEach condition is based on the file name, path, extension, UTI (the kind of file, like image which is useful for finding vector assets), Workspace location (namely groups), or source control status (handy for finding newly applied changes.)
\nMost of my conditions are file-name-based. And for those, you get the following matching conditions. The \u201cends with\u201d is an obvious win for extensions (although you can also use UTIs for that), and \u201cstarts with\u201d can help for projects organized in hierarchical ways.
\nNow, interestingly enough, this list fails to offer \u201cdoes not contain\u201d but that\u2019s fairly easy to work around. Since Xcode supports regex matching, you can easily replicate \u201cdoes not contain\u201d with an appropriate regex:
\n\nChange the file name to a path to exclude source file directories.
\nYou can create as many search domains as you like. At least, I haven\u2019t found an upper bound yet. I haven\u2019t found a way to reorder the find scopes, although if you\u2019re really controlling about this, you can pop into \u00a0your workspace (ProjectName.xcodeproj/project.xcworkspace/xcuserdata/username.xcuserdatad
), convert your UserInterfaceState.xcuserstate
to xml (plutil -convert xml1
), and hand-edit it the way you need.
There are lots of wonderful little Xcode tweaks like these throughout this monster of an IDE. What are some of your faves? If I have time this week, I\u2019ll share some of mine, such as the four-square \u2014 another of my favorite tools \u2014 and a few great ways to connect your editor to the navigator.
" }, "unread": false, "readTime": 3406, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8", "label": "THree" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" } ], "actionTimestamp": 1572499929438 }, { "keywords": [ "Apple" ], "originId": "https://9to5mac.com/?p=617536", "recrawled": 1572455791719, "updateCount": 1, "fingerprint": "77224d8", "id": "BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16e1d065ad9:1c0f1:d4506071", "author": "Michael Potuck", "summary": { "direction": "ltr", "content": "Sonos has announced a new initiative today that makes it easy for existing customers to trade in older Sonos products for a nice discount on new ones. For Apple customers, the Trade Up program is a neat opportunity to bring Sonos\u2019 AirPlay 2 compatible speakers into your home.
\n\nThe post Sonos pushing AirPlay 2 speaker lineup with new Trade Up program appeared first on 9to5Mac.
" }, "alternate": [ { "href": "https://9to5mac.com/2019/10/30/sonos-trade-in-program-airplay-2/", "type": "text/html" } ], "crawled": 1572444986073, "title": "Sonos pushing AirPlay 2 speaker lineup with new Trade Up program", "published": 1572442686000, "origin": { "streamId": "feed/http://9to5mac.com/feed/", "htmlUrl": "https://9to5mac.com", "title": "9to5Mac" }, "visual": { "url": "https://cdn.vox-cdn.com/thumbor/06e5FJWgUfUSmDaPJIEZoGF1XOs=/0x68:2040x1136/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/10378819/DSCF3031.jpg", "width": 1200, "height": 628, "contentType": "image/jpeg" }, "unread": false, "readTime": 4493, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815", "label": "Macintosh" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" } ], "actionTimestamp": 1572499889443 }, { "originId": "tag:blogger.com,1999:blog-8954608646904080796.post-4991449931465752891", "fingerprint": "7d99be14", "thumbnail": [ { "url": "https://1.bp.blogspot.com/-rGjQOQb89a8/XbS25YYbgeI/AAAAAAAADUI/s8Q4e0pTtX4qSTP3wrkt13VGV53Q8J0tQCLcBGAsYHQ/s72-c/092.jpg", "width": 72, "height": 72 } ], "id": "v0v+7Ya8tssIZvd3/pcnFRr3HwvY/5YK3FGc2t65c0Y=_16e0a232509:17e32:d4506071", "updated": 1572124724480, "author": "Edward Feser", "alternate": [ { "href": "http://edwardfeser.blogspot.com/2019/10/john-paul-ii-in-defense-of-nation-and.html", "type": "text/html" } ], "crawled": 1572128105737, "title": "John Paul II in defense of the nation and patriotism", "published": 1572124680000, "origin": { "streamId": "feed/http://edwardfeser.blogspot.com/feeds/posts/default", "htmlUrl": "http://edwardfeser.blogspot.com/", "title": "Edward Feser" }, "content": { "direction": "ltr", "content": "Some people took my post No ETAs as if I were arguing against doing software estimates of any kind, ever.
\nI didn\u2019t actually mean that. If your boss, project manager, or person you\u2019re contracting with asks for an estimate, do your best to come up with something accurate. If you\u2019re writing enterprise software, you may even be contractually bound to provide estimates for when features will ship.
\nThere are ways to get pretty good at this. Pay attention to history and avoid wishful thinking. Don\u2019t assume perfect productivity. Allow for the unexpected, because there\u2019s always something.
\nWhat I\u2019m talking about is the case where you\u2019re writing a consumer-facing app \u2014\u00a0something that would get published on an app store, for instance \u2014\u00a0and customers or potential customers ask about an ETA for a given feature. Don\u2019t do it! (For the reasons stated in the article.)
" }, "alternate": [ { "href": "https://inessential.com/2019/10/30/etas_follow_up", "type": "text/html" } ], "crawled": 1572467543710, "title": "ETAs: Follow-Up", "published": 1572466202000, "origin": { "streamId": "feed/http://ranchero.com/xml/rss.xml", "htmlUrl": "https://inessential.com/", "title": "inessential.com" }, "unread": false, "readTime": 6552, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8", "label": "THree" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" } ], "actionTimestamp": 1572499788300 }, { "keywords": [ "Apple" ], "originId": "https://9to5mac.com/?p=617593", "fingerprint": "a1dafaae", "id": "BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16e1de23321:1c6e3:d4506071", "author": "Guilherme Rambo", "summary": { "direction": "ltr", "content": "Rumors about a new 16-inch MacBook Pro are not exactly new, with recent icon evidence found in macOS Catalina betas suggesting the redesign mentioned in some reports is not going to happen as we thought.
\n\nThe post Exclusive: 16-inch MacBook Pro Touch Bar and Touch ID layout confirmed appeared first on 9to5Mac.
" }, "alternate": [ { "href": "https://9to5mac.com/2019/10/30/exclusive-16-inch-macbook-pro-touch-bar-and-touch-id-layout-confirmed/", "type": "text/html" } ], "crawled": 1572459393825, "title": "Exclusive: 16-inch MacBook Pro Touch Bar and Touch ID layout confirmed", "published": 1572456327000, "origin": { "streamId": "feed/http://9to5mac.com/feed/", "htmlUrl": "https://9to5mac.com", "title": "9to5Mac" }, "unread": false, "readTime": 3947, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815", "label": "Macintosh" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" } ], "actionTimestamp": 1572499186128 }, { "originId": "https://inessential.com/2019/10/30/you_choose_follow_up", "fingerprint": "f1dd63ab", "id": "+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16e1f71bf1c:1d086:d4506071", "summary": { "direction": "ltr", "content": "It came to my attention after writing my blog post about how we choose the web we want\u00a0that the pessimism is about not being able to make a living from blogging.
\nHere\u2019s my followup: I don\u2019t care. Bite me.
" }, "alternate": [ { "href": "https://inessential.com/2019/10/30/you_choose_follow_up", "type": "text/html" } ], "crawled": 1572485578524, "title": "You Choose: Follow-Up", "published": 1572482100000, "origin": { "streamId": "feed/http://ranchero.com/xml/rss.xml", "htmlUrl": "https://inessential.com/", "title": "inessential.com" }, "unread": false, "readTime": 3663, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8", "label": "THree" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" } ], "actionTimestamp": 1572499146275 }, { "originId": "https://inessential.com/2019/10/14/netnewswire_os_compatibility_strategy", "fingerprint": "fa6e6fff", "id": "+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16dccdfc972:a6e5:d4506071", "summary": { "direction": "ltr", "content": "We have two goals with the app: 1) get as many people using RSS as possible, and 2) make the best app we can.
\nTo reach #2 \u2014\u00a0making the best app we can \u2014\u00a0we need to do a couple things. One is stay modern: use new APIs and tools that make the app better and easier to maintain. A second is to not spend time on things that don\u2019t make the app better. A third is to attract and retain contributors, who are usually more psyched to work with modern stuff than with old stuff.
\nYou can see how that\u2019s in a little bit of conflict with #1 (getting as many people as possible using RSS readers).
\nAfter a major OS update, we will switch to requiring that update on our next major release \u2014\u00a0where major is defined as something like 5.0 or 5.1, but not something like 5.0.1. (In other words: the upcoming NetNewsWire 5.0.3 release will run on Mojave, while NetNewsWire 5.1 will require Catalina.)
\nAt the same time, we will make older versions available via the website. For instance, the last version that will run on Mojave will likely be 5.0.4 (which isn\u2019t finished yet) \u2014 and we\u2019ll make that version available indefinitely for people who haven\u2019t upgraded to Catalina.
\nThis will mean that people running older OSes will still get a high-quality app \u2014 it\u2019s just that it won\u2019t have the latest features.
\nThe key is that this allows us to make NetNewsWire the best app it can be, and making the best app we can is also part of furthering the goal of getting as many people as possible using RSS. (The biggest part, in fact. Bigger than compatibility with older OSes.)
\nWhile I know this will disappoint some people, I hope you\u2019ll understand why we decided to do it this way. Decisions like this are never easy \u2014 there are always conflicting values to weigh, pros and cons and add up \u2014\u00a0and we don\u2019t make them impulsively. But making NetNewsWire the best app it can be has to be job #1.
" }, "alternate": [ { "href": "https://inessential.com/2019/10/14/netnewswire_os_compatibility_strategy", "type": "text/html" } ], "crawled": 1571100281202, "title": "NetNewsWire OS Compatibility Strategy", "published": 1571099404000, "origin": { "streamId": "feed/http://ranchero.com/xml/rss.xml", "htmlUrl": "https://inessential.com/", "title": "inessential.com" }, "unread": false, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8", "label": "THree" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" } ], "actionTimestamp": 1571123677415 }, { "keywords": [ "Xcode" ], "originId": "https://nshipster.com/swiftui-previews", "recrawled": 1571407228188, "updateCount": 2, "fingerprint": "7582ddff", "id": "08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16dc8d7749a:96ed:d4506071", "updated": 1571036400000, "author": "Mattt", "summary": { "direction": "ltr", "content": "Working on a large iOS codebase often involves a lot of waiting. But with Xcode 11, our wait is finally over \u2014 and it\u2019s all thanks to SwiftUI.
" }, "alternate": [ { "href": "https://nshipster.com/swiftui-previews/", "type": "text/html" } ], "crawled": 1571032626330, "title": "SwiftUI Previews on macOS Catalina and Xcode 11", "published": 1571036400000, "origin": { "streamId": "feed/http://nshipster.com/feed.xml", "htmlUrl": "https://nshipster.com/", "title": "NSHipster" }, "content": { "direction": "ltr", "content": "Working on a large iOS codebase often involves a lot of waiting:\nWaiting for Xcode to index your files,\nwaiting for Swift and Objective-C code to compile,\nwaiting for the Simulator to boot and your app to launch\u2026
\nAnd after all of that,\nyou spend even more time getting your app\ninto a particular state and onto a particular screen,\njust to see whether the Auto Layout constraint you just added\nfixes that regression you found.\nIt didn\u2019t, of course,\nso you jump back into Xcode,\ntweak the Content Hugging Priority,\nhit \u2318R,\nand start the whole process again.
\nWe might relate our sorry predicament to\nthat one xkcd comic,\nbut for those of us who don\u2019t so much relish in\nthe stop-and-go nature of app development,\nthere\u2019s an old Yiddish joke about Shlemiel the painter\n(provided below with a few \uf8ff-specific modifications;\nfor the uninitiated,\nplease refer to Joel Spolsky\u2019s\noriginal telling):
\n\n\nShlemiel gets a job as a software developer,\nimplementing a new iOS app.\nOn the first sprint he opens Xcode\nand implements 10 new screens of the app.\n\u201cThat\u2019s pretty good!\u201d says his manager,\n\u201cyou\u2019re a fast worker!\u201d and pays him a Bitcoin.
\nThe next sprint Shlemiel only gets 5 screens done.\n\u201cWell, that\u2019s not nearly as good as yesterday,\nbut you\u2019re still a fast worker. 5 screens is respectable,\u201d\nand pays him a Bitcoin.
\nThe next sprint Shlemiel implements 1 screen.\n\u201cOnly 1!\u201d shouts his manager.\n\u201cThat\u2019s unacceptable!\nOn the first day you did ten times that much work!\nWhat\u2019s going on?\u201d
\n\u201cI can\u2019t help it,\u201d says Shlemiel.\n\u201cEach sprint I get further and further away from\n
\napplication(_:did
!\u201dFinish Launching With Options:)
Over the years,\nthere have been some developments that\u2019ve helped things slightly,\nincluding\n@IBInspectable
and @IBDesignable
\nand Xcode Playgrounds.\nBut with Xcode 11,\nour wait is finally over \u2014\nand it\u2019s all thanks to SwiftUI.
Although many of us have taken a \u201cwait and see\u201d approach to SwiftUI,\nwe can start using its capabilities today\nto radically speed up and improve our development process \u2014\nwithout changing a line of code in our UIKit apps.
\nConsider a subclass of UIButton
\nthat draws a border around itself:
final class BorderedButton : UIButton {\n var cornerRadius : CGFloat { ... }\n var borderWidth : CGFloat { ... }\n var borderColor : UIColor? { ... }\n }\n
\nNormally,\nif we wanted to test how our UI element performs,\nwe\u2019d have to add it to a view in our app,\nbuild and run,\nand navigate to that screen.\nBut with Xcode 11,\nwe can now see a preview side-by-side with the code editor\nby adding the following under the original declaration of Bordered
:
#if canImport(SwiftUI) && DEBUG \n import SwiftUI \n @available(iOS 13.0, *) \n struct BorderedButton_Preview : PreviewProvider {\n static var previews: some View {\n UIViewPreview {\n let button = BorderedButton (frame: .zero)\n button.setTitle ("Follow", for: .normal)\n button.tintColor = .systemOrange \n button.setTitleColor (.systemOrange , for: .normal)\n return button\n }.previewLayout (.sizeThatFits )\n .padding(10)\n }\n }\n #endif\n
\n\nUsing a new feature called dynamic replacement,\nXcode can update this preview without recompiling \u2014\nwithin moments of your making a code change.\nThis lets you rapidly prototype changes like never before.
\nWant to see how your button handles long titles?\nBang away on your keyboard within the call to set
\nin your preview,\nand test out potential fixes in your underlying implementation\nwithout so much as leaving your current file!
Let\u2019s say our app had a Favorite
\u2014\na distant cousin (perhaps by composition) to Bordered
.\nIn its default state,\nit shows has the title \u201cFavorite\u201d\nand displays a \u2661 icon.\nWhen its is
property is set to true
,\nthe title is set to \u201cUnfavorite\u201d\nand displays a \u2661\u0338 icon.
We can preview both at once\nby wrapping two UIView
instances within a single SwiftUI Group
:
Group {\n UIViewPreview {\n let button = FavoriteButton (frame: .zero)\n return button\n }\n UIViewPreview {\n let button = FavoriteButton (frame: .zero)\n button.isFavorited = true\n return button\n }\n }.previewLayout (.sizeThatFits )\n .padding(10)\n
\n\nWith Dark Mode in iOS 13,\nit\u2019s always a good idea to double-check that your custom views\nare configured with dynamic colors\nor accommodate both light and dark appearance in some other way.
\nAn easy way to do this\nwould be to use a For
element\nto render a preview for each case in the Color
enumeration:
ForEach (ColorScheme .allCases , id: \\.self) { colorScheme in\n UIViewPreview {\n let button = BorderedButton (frame: .zero)\n button.setTitle ("Subscribe", for: .normal)\n button.setImage (UIImage(systemName : "plus"), for: .normal)\n button.setTitleColor (.systemOrange , for: .normal)\n button.tintColor = .systemOrange \n return button\n }.environment(\\.colorScheme , colorScheme )\n .previewDisplayName ("\\(colorScheme )")\n }.previewLayout (.sizeThatFits )\n .background(Color(.systemBackground ))\n .padding(10)\n
\n\nWe can use the same approach to preview our views in various\nDynamic Type Sizes:
\nForEach (ContentSizeCategory .allCases , id: \\.self) { sizeCategory in\n UIViewPreview {\n let button = BorderedButton (frame: .zero)\n button.setTitle ("Subscribe", for: .normal)\n button.setImage (UIImage(systemName : "plus"), for: .normal)\n button.setTitleColor (.systemOrange , for: .normal)\n button.tintColor = .systemOrange \n return button\n }.environment(\\.sizeCategory , sizeCategory )\n .previewDisplayName ("\\(sizeCategory )")\n }.previewLayout (.sizeThatFits )\n .padding(10)\n
\n\nXcode Previews are especially time-saving when it comes to\nlocalizing an app into multiple languages.\nCompared to the hassle of configuring Simulator\nback and forth between different languages and regions,\nthis new approach makes a world of difference.
\nLet\u2019s say that, in addition to English,\nyour app supported various right-to-left languages.\nYou could verify that your\nRTL logic worked as expected like so:
\nlet supportedLocales : [Locale] = [\n "en-US", // English (United States)\n "ar-QA", // Arabic (Qatar)\n "he-IL", // Hebrew (Israel)\n "ur-IN" // Urdu (India)\n ].map(Locale.init(identifier:))\n func localizedString (_ key: String, for locale: Locale) -> String? { ... }\n return ForEach (supportedLocales , id: \\.identifier) { locale in\n UIViewPreview {\n let button = BorderedButton (frame: .zero)\n button.setTitle (localizedString ("Subscribe", for: locale), for: .normal)\n button.setImage (UIImage(systemName : "plus"), for: .normal)\n button.setTitleColor (.systemOrange , for: .normal)\n button.tintColor = .systemOrange \n return button\n }.environment(\\.locale, locale)\n .previewDisplayName (Locale.current.localizedString (forIdentifier : locale.identifier))\n }.previewLayout (.sizeThatFits )\n .padding(10)\n
\n\nSwiftUI previews aren\u2019t limited to views,\nyou can also use them with view controllers.\nBy creating a custom UIView
type\nand taking advantage of some\nnew UIStoryboard
class methods in iOS 13,\nwe can easily preview our view controller\non various devices \u2014\none on top of another:
#if canImport(SwiftUI) && DEBUG \n import SwiftUI \n let deviceNames : [String] = [\n "iPhone SE" ,\n "iPad 11 Pro Max" ,\n "iPad Pro (11-inch)" \n ]\n @available(iOS 13.0, *) \n struct ViewController_Preview : PreviewProvider {\n static var previews: some View {\n ForEach (deviceNames , id: \\.self) { deviceName in\n UIViewControllerPreview {\n UIStoryboard(name: "Main", bundle: nil)\n .instantiateInitialViewController { coder in\n ViewController (coder: coder)\n }!\n }.previewDevice (PreviewDevice (rawValue : deviceName ))\n .previewDisplayName (deviceName )\n }\n }\n }\n #endif\n
\n\nAlthough most of us are still some years away from shipping SwiftUI in our apps\n(whether by choice or necessity),\nwe can all immediately benefit from the order-of-magnitude improvement\nit enables with Xcode 11 on macOS Catalina.
\nBy eliminating so much time spent waiting for things to happen,\nwe not only get (literally) hours more time each week,\nbut we unlock the possibility of maintaining an unbroken flow state during that time.\nNot only that,\nbut the convenience of integrated tests\nfundamentally changes the calculus for testing:\ninstead of being a rare \u201cnice to have,\u201d\nthey\u2019re the new default.\nPlus:\nthese inline previews serve as living documentation\nthat can help teams both large and small\nfinally get a handle on their design system.
\nIt\u2019s hard to overstate how much of a game-changer Xcode Previews are for iOS development,\nand we couldn\u2019t be happier to incorporate them into our workflow.
\n" }, "visual": { "url": "none" }, "unread": false, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5", "label": "Programming" } ], "tags": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved", "label": "Saved For Later" }, { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read", "label": "" } ], "actionTimestamp": 1571037592868 }, { "originId": "tag:blogger.com,1999:blog-8954608646904080796.post-3215871338266756283", "fingerprint": "717870dc", "thumbnail": [ { "url": "https://1.bp.blogspot.com/-JTONCNpv2X8/XaEUgnGZSzI/AAAAAAAADTg/EaM5cDjkD3kRnXPIR0-6AX-3VxlC_br_QCEwYBhgL/s72-c/091.jpg", "width": 72, "height": 72 } ], "id": "v0v+7Ya8tssIZvd3/pcnFRr3HwvY/5YK3FGc2t65c0Y=_16dbd619e56:82fa:d4506071", "updated": 1570837682218, "author": "Edward Feser", "alternate": [ { "href": "http://edwardfeser.blogspot.com/2019/10/around-web.html", "type": "text/html" } ], "crawled": 1570840354390, "title": "Around the web", "published": 1570837680000, "origin": { "streamId": "feed/http://edwardfeser.blogspot.com/feeds/posts/default", "htmlUrl": "http://edwardfeser.blogspot.com/", "title": "Edward Feser" }, "content": { "direction": "ltr", "content": "