{
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved",
"items": [
{
"originId": "tag:blogger.com,1999:blog-8954608646904080796.post-8772112161384908748",
"fingerprint": "80075e70",
"thumbnail": [
{
"url": "https://1.bp.blogspot.com/-pMon836tLyo/XatsZ0Tv9MI/AAAAAAAADTs/90dBB5-ohkk2yOW4--zh7M0p_JicFuC4QCLcBGAsYHQ/s72-c/017.jpg",
"width": 72,
"height": 72
}
],
"id": "v0v+7Ya8tssIZvd3/pcnFRr3HwvY/5YK3FGc2t65c0Y=_16de5ccc6bb:ffbc:d4506071",
"updated": 1571515782582,
"author": "Edward Feser",
"alternate": [
{
"href": "http://edwardfeser.blogspot.com/2019/10/masculinity-and-marvel-movies.html",
"type": "text/html"
}
],
"crawled": 1571518465723,
"title": "Masculinity and the Marvel movies",
"published": 1571515740000,
"origin": {
"streamId": "feed/http://edwardfeser.blogspot.com/feeds/posts/default",
"htmlUrl": "http://edwardfeser.blogspot.com/",
"title": "Edward Feser"
},
"content": {
"direction": "ltr",
"content": "
\nUnder the argument for an iPhone subscription, which some people call Apple Prime after the Amazon program of the same name, Apple would bundle hardware upgrades with services like iCloud storage or Apple TV+ content and hardware for a single monthly fee. This would let it switch iPhone sales from a transactional model to a subscription model, potentially driving the stock price up without having to increase product sales or prices dramatically.\n\nAnd:\n
\n\u2033In terms of hardware as a service or as a bundle, if you will, there are customers today that essentially view the hardware like that because they\u2019re on upgrade plans and so forth,\u201d Cook said during an earnings call. \u201cSo to some degree that exists today.\u201d\n\nAnd, most importantly:\n
\n\u201cMy perspective is that will grow in the future to larger numbers. It will grow disproportionately\u201d\n\nI had the chance to be on John Gruber's show (recorded yesterday, guessing it'll drop today or tomorrow, assuming John is not too horrified with the results), and we were talking about this, peripherally. John mentioned the future possibility of Apple Prime, a concept similar to Amazon Prime. From the article:\n
\nUnder the argument for an iPhone subscription, which some people call Apple Prime after the Amazon program of the same name, Apple would bundle hardware upgrades with services like iCloud storage or Apple TV+ content and hardware for a single monthly fee.\n\nI suspect we'll all eventually be subscribing from a menu of services, including column A, software, column B, traditional services, and column C, hardware. Intriguing.\u221e Read this on The Loop" }, "alternate": [ { "href": "https://www.cnbc.com/2019/10/30/apple-lays-groundwork-for-iphone-or-apple-prime-subscription.html", "type": "text/html" } ], "crawled": 1572535435674, "title": "Apple is laying the groundwork for an iPhone subscription", "published": 1572532001000, "origin": { "streamId": "feed/http://www.loopinsight.com/feed/", "htmlUrl": "https://www.loopinsight.com", "title": "The Loop" }, "content": { "direction": "ltr", "content": "
Kif Leswing, CNBC:
\n\n\nUnder the argument for an iPhone subscription, which some people call Apple Prime after the Amazon program of the same name, Apple would bundle hardware upgrades with services like iCloud storage or Apple TV+ content and hardware for a single monthly fee. This would let it switch iPhone sales from a transactional model to a subscription model, potentially driving the stock price up without having to increase product sales or prices dramatically.
\n
And:
\n\n\n\u2033In terms of hardware as a service or as a bundle, if you will, there are customers today that essentially view the hardware like that because they\u2019re on upgrade plans and so forth,\u201d Cook said during an earnings call. \u201cSo to some degree that exists today.\u201d
\n
And, most importantly:
\n\n\n\u201cMy perspective is that will grow in the future to larger numbers. It will grow disproportionately\u201d
\n
I had the chance to be on John Gruber\u2019s show (recorded yesterday, guessing it\u2019ll drop today or tomorrow, assuming John is not too horrified with the results), and we were talking about this, peripherally. John mentioned the future possibility of Apple Prime, a concept similar to Amazon Prime. From the article:
\n\n\nUnder the argument for an iPhone subscription, which some people call Apple Prime after the Amazon program of the same name, Apple would bundle hardware upgrades with services like iCloud storage or Apple TV+ content and hardware for a single monthly fee.
\n
I suspect we\u2019ll all eventually be subscribing from a menu of services, including column A, software, column B, traditional services, and column C, hardware. Intriguing.
\n" }, "visual": { "url": "https://cdn.vox-cdn.com/thumbor/fhKs50EFS0SJPjSjfCR7lXwcMfs=/0x0:2040x1360/1310x873/cdn.vox-cdn.com/uploads/chorus_image/image/59667903/acastro_180508_1777_google_IO_0002.0.jpg", "width": 1310, "height": 873, "contentType": "image/jpeg" }, "unread": false, "readTime": 20737, "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": 1572578952687 }, { "keywords": [ "Apple" ], "originId": "https://9to5mac.com/?p=617610", "fingerprint": "48affe9c", "id": "BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16e1de23321:1c6e5:d4506071", "author": "Chance Miller", "summary": { "direction": "ltr", "content": "\nMicrosoft is planning several updates to its Outlook app for iPhone and iPad. As explained by The Verge, Microsoft will roll out features such as iPad Split View, smart folders, and more to the Outlook app over the coming weeks.
\n\nThe post Microsoft Outlook for iPhone and iPad adding Split View, Do Not Disturb, more appeared first on 9to5Mac.
" }, "alternate": [ { "href": "https://9to5mac.com/2019/10/30/microsoft-outlook-split-view-more/", "type": "text/html" } ], "crawled": 1572459393825, "title": "Microsoft Outlook for iPhone and iPad adding Split View, Do Not Disturb, more", "published": 1572458509000, "origin": { "streamId": "feed/http://9to5mac.com/feed/", "htmlUrl": "https://9to5mac.com", "title": "9to5Mac" }, "unread": false, "readTime": 13858, "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": 1572578905515 }, { "keywords": [ "Miscellaneous" ], "originId": "https://nshipster.com/device-identifiers", "fingerprint": "5e66b947", "id": "08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16e22ccc11a:1e495:d4506071", "updated": 1572505200000, "author": "Mattt", "summary": { "direction": "ltr", "content": "For every era, there\u2019s a monster that embodies the anxieties of the age.
" }, "alternate": [ { "href": "https://nshipster.com/device-identifiers/", "type": "text/html" } ], "crawled": 1572541874458, "title": "Device Identifiers and Fingerprinting on iOS", "published": 1572505200000, "origin": { "streamId": "feed/http://nshipster.com/feed.xml", "htmlUrl": "https://nshipster.com/", "title": "NSHipster" }, "content": { "direction": "ltr", "content": "For every era,\nthere\u2019s a monster that embodies the anxieties of the age.
\nAt the dawn of the Holocene,\nour ancestors traced the contours of shadows cast by the campfire\nas they kept watch over the darkness.\nOnce we learned to read the night sky for navigation,\nsailors swapped stories of sea creatures like\nLeviathan and\nSiren\nto describe the dangers of open ocean\n(and the perils to be found on unfamiliar shores).
\nFrankenstein\u2019s monster\nwas as much the creation of Mary Shelley\nas it was a spiritual collaboration with\nLuigi Galvani.\nAnd Bram Stoker\u2019s\nfictionalized account of the mummy\u2019s curse\nwas more a response to the\nEgyptomania\nand European colonialism\nof the nineteenth century\nthan any personal account of the Middle Kingdom.
\nMore recently,\nthe \u201cmonster ruins a beach party\u201d\ntrope of the 1960s\narose from concerns of teenager morality.\nWhile the\nMartians\nwho invaded those same drive-in double features\nserved as a proxy for Cold War fears at the height of the\nSpace Race.
\nAll of which begs the question:\n\u201cWhat monster best exemplifies our present age?\u201d
\nConsider the unnamed monster from the film\nIt Follows:\na formless, supernatural being that relentlessly pursues its victims\nanywhere on the planet.
\nSounds a bit like the state of\nad tech\nin 2019, no?
\n\nThis week on NSHipster \u2014\nin celebration of our favorite holiday\n\ud83c\udf83 \u2014\nwe\u2019re taking a look at the myriad ways that\nyou\u2019re being tracked on iOS,\nboth sanctioned and unsanctioned,\nhistorically and presently.\nSo gather around the campfire,\nand allow us to trace the contours of the unseen, formless monsters\nthat stalk us under cover of Dark Mode.
\nContrary to our intuitions about natural selection in the marketplace,\nhistory is littered with examples of\ninferior-but-better-marketed products winning out over superior alternatives:\nVHS vs. Betamax,\nWindows vs. Macintosh,\netc.\n(According to the common wisdom of business folks, at least.)\nRegardless,\nmost companies reach a point where\n\u201cif you build it, they will come\u201d\nceases to be a politically viable strategy,\nand someone authorizes a marketing budget.
\nMarketers are tasked with growing market share\nby identifying and communicating with as many potential customers as possible.\nAnd many \u2014\neither out of a genuine belief or formulated as a post hoc rationalization \u2014\ntake the potential benefit of their product\nas a license to flouting long-established customs of personal privacy.\nSo they enlist the help of one or more\nadvertising firms,\nwho promise to maximize their allocated budget and\nprovide some accountability for their spending\nby using technology to\ntarget,\ndeliver, and\nanalyze\nmessaging to consumers.
\nEach of these tasks is predicated on a consistent identity,\nwhich is why advertisers go to such great lengths to track you.
\nApple\u2019s provided various APIS to facilitate user identification\nfor various purposes:
\nIn the early days of iOS,\nApple provided a unique
property on UIDevice
\u2014\naffectionately referred to as a\nUDID\n(not to be confused with a UUID).\nAlthough such functionality seems unthinkable today,\nthat property existed until iOS 5,\nuntil it was\ndeprecated and replaced by identifier
in iOS 6.
Starting in iOS 6,\ndevelopers can use the\nidentifier
property on UIDevice
\nto generate a unique identifier that\u2019s shared across apps and extensions\ncreated by the same vendor\n(IDFV).
import UIKit\n let idfv = UIDevice.current.identifierForVendor // BD43813E-CFC5-4EEB-ABE2-94562A6E76CA\n
\n\nAlong with identifier
came the introduction of a new\nAdSupport framework,\nwhich Apple created to help distinguish\nidentification necessary for app functionality\nfrom anything in the service of advertising.
The resulting\nadvertisingidentifier
property\n(affectionately referred to as\nIDFA by its associates)\ndiffers from identifier
\nby returning the same value for everyone.\nThe value can change, for example,\nif the user resets their Advertising Identifier\nor erases their device.
import AdSupport \n let idfa = ASIdentifierManager .shared().advertisingIdentifier \n
\nIf advertising tracking is limited,\nthe property returns a zeroed-out UUID instead.
\nidfa.uuidString == "00000000-0000-0000-0000-000000000000" // true if the user has limited ad tracking\n
\n\n\nidentifier
and advertising
\nprovide all the same functionality as the unique
property\nthey replaced in iOS 6,\nsave for one:\nthe ability to persist across device resets and app uninstalls.
In iOS 11,\nApple quietly introduced the\nDeviceCheck framework,\nwhich allows developers to assign two bits of information\nthat are persisted by Apple\nuntil the developer manually removes them.
\nInteracting with the DeviceCheck framework should be familiar to\nanyone familiar with APNS:\nafter setting things up on App Store Connect and your servers,\nthe client generates tokens on the device,\nwhich are sent to your servers to set or query two bits of information:
\nimport DeviceCheck \n let device = DCDevice.current\n if device.isSupported {\n device.generateToken { data, error in\n if let token = data?.base64EncodedString () {\n send token to your server\n }\n }\n }\n
\nBased on the device token and other information sent by the client,\nthe server tells Apple to set each bit value\nby sending a JSON payload like this:
\n{\n"device_token": "QTk4QkFDNEItNTBDMy00Qjc5LThBRUEtMDQ5RTQzRjNGQzU0Cg==" ,\n"transaction_id": "D98BA630-E225-4A2F-AFEC-BE3A3D591708",\n"timestamp": 1572531720,\n"bit0": true,\n"bit1": false\n}\n
\nTo retrieve those two bits at a later point in time,\nthe server sends a payload without bit0
and bit1
fields:
{\n"device_token": "QTk4QkFDNEItNTBDMy00Qjc5LThBRUEtMDQ5RTQzRjNGQzU0Cg==" ,\n"transaction_id": "D98BA630-E225-4A2F-AFEC-BE3A3D591708",\n"timestamp": 1572532500\n}\n
\nIf everything worked,\nApple\u2019s servers would respond with a 200
status code\nand the following JSON payload:
{\n"bit0" : true\n"bit1" : false,\n"last_update_time" : "2019-10"\n}\n
\n\nDespite these affordances by Apple,\nadvertisers have continued to work to circumvent user privacy protections\nand use any and all information at their disposal\nto identify users by other means.
\nOver the years,\nApple\u2019s restricted access to information about\ndevice hardware,\ninstalled apps,\nnearby WiFi networks.\nThey\u2019ve required apps to request permission to\nget your current location,\naccess your camera and microphone,\nflip through your contacts, and\nfind and connect to Bluetooth accessories.\nThey\u2019ve taken bold steps to prevent user tracking in Safari.
\nFor lack of this information,\ncompanies have had to get creative,\nlooking to forge unique identities from the scraps of what\u2019s still available.\nThis process of identification by a combination of external factors\nis known as fingerprinting.
\nThe unfortunate reality is that we can be uniquely identified\nby vanishingly small amounts of information.\nFor example,\nindividuals within a population can be singled out by as few as\nfour timestamped coordinates\n(de Montjoye, Hidalgo, Verleysen, & Blondel, 2013)\nor little more than a birthday and a ZIP code\n(Sweeney, 2000).
\nEvery WWDC since 2012 has featured a session about Privacy,\nbut the only mention of fingerprinting specifically was\na brief discussion in 2014\nabout how to avoid doing it.
\nBy our count,\na determined party could use conventional, unrestricted APIs\nto generate a few dozen bits of randomness:
\nLocale information is the greatest source of identifying information\non Apple platforms.\nThe combination of your\npreferred languages, region, calendar, time zone,\nand which keyboards you have installed\nsay a lot about who you are \u2014\nespecially if you have less conventional preferences.
\nimport Foundation\n Locale.current.languageCode \n log2(Double(Locale.isoLanguageCodes .count)) // 9.217 bits\n Locale.current.regionCode \n log2(Double(Locale.isoRegionCodes .count)) // 8 bits\n Locale.current.calendar.identifier\n // ~2^4 (16) Calendars\n TimeZone .current.identifier\n log2(Double(TimeZone .knownTimeZoneIdentifiers .count)) // 8.775 bits\n UserDefaults .standard.object(forKey : "AppleKeyboards" )\n // ~2^6 (64) iOS keyboards \n
\n\nAccessibility preferences also provide a great deal of information,\nwith each individual setting contributing a single potential bit:
\nUIAccessibility.isBoldTextEnabled \n UIAccessibility.isShakeToUndoEnabled \n UIAccessibility.isReduceMotionEnabled \n UIAccessibility.isDarkerSystemColorsEnabled \n UIAccessibility.isReduceTransparencyEnabled \n UIAccessibility.isAssistiveTouchRunning \n
\nOf the approximately ~25% of users who take advantage of\nDynamic Type\nby configuring a preferred font size,\nthat selection may also be used to fingerprint you:
\nlet application = UIApplication.shared\n application.preferredContentSizeCategory \n
\nAlthough most of the juiciest bits have been locked down\nin OS updates over the years,\nthere\u2019s just enough to contribute a few more bits for purposes of identification.
\nOn iOS,\nyou can get the current model and amount of storage of a user\u2019s device:
\nimport UIKit\n let device = UIDevice.current\n device.name // "iPhone 11 Pro" \n let fileManager = FileManager .default\n if let path = fileManager .urls(for: .libraryDirectory , in: .systemDomainMask ).last?.path,\n let systemSize = try? fileManager .attributesOfFileSystem (forPath : path)[.systemSize ] as? Int\n {\n Measurement<UnitInformationStorage >(value: Double(systemSize ), unit: .bytes)\n .converted(to: .gigabytes) // ~256GB\n }\n
\nWith 14 supported iOS devices,\nmost having 3 configurations each,\nlet\u2019s say that this contributes about 32 possibilities, or 5 bits.
\nYou can go a few steps further on macOS,\nto further differentiate hardware by its processor count and amount of RAM:
\nprocessInfo .processorCount // 8\n Measurement<UnitInformationStorage >(value: Double(processInfo .physicalMemory ),\n unit: .bytes)\n .converted(to: .gigabytes) // 16GB\n
\nIt\u2019s hard to get a sense of\nhow many different Mac models are in use,\nbut a reasonable estimate would be on the order of 26 or 27.
\nKnowing whether someone\u2019s phone is on Verizon or Vodaphone\ncan also be factored into a fingerprint.\nYou can use the CTTelephony
class from the\nCoreTelephony framework\nto lookup the providers for devices with cellular service:
import CoreTelephony \n let networkInfo = CTTelephonyNetworkInfo ()\n let carriers = networkInfo .serviceSubscriberCellularProviders ?.values\n carriers?.map { ($0.mobileNetworkCode , $0.mobileCountryCode ) }\n
\nThe number of providers varies per country,\nbut using the 4 major carriers in United States\nas a guideline,\nwe can say carrier information would contribute about 2 bits\n(or more if you have multiple SIM cards installed).
\nMore generally,\neven knowing whether someone can send texts or email at all\ncan be factored into a fingerprint.\nThis information can be gathered without permissions via\nthe MessageUI framework.
\nimport MessageUI \n MFMailComposeViewController .canSendMail ()\n MFMessageComposeViewController .canSendText ()\n
\nIf the use of digital fingerprinting seems outlandish,\nthat\u2019s just scratching the surface of how companies and researchers\nhave figured out how to circumvent your privacy.
\nAlthough access to geolocation through conventional APIs\nrequires explicit authorization,\nthird parties may be able to get a general sense of where you are in the world\nbased on how you access the Internet.
\nGeolocation by source IP address\nis used extensively for things like region locking and localization.\nYou could also combine this information with\nping-time measurements\nto hosts in known locations\nto get a more accurate pinpoint on location (Weinberg, Cho, Christin, Sekar, & Gill, 2018):
\nping -c 5 99.24.18.13 # San Francisco, USA\n \n--- 99.24.18.13 ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 11.900/12.184/12.895/0.380 ms\nping -c 5 203.47.10.37 # Adelaide, Australia\n \n--- 203.47.10.37 ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 202.122/202.433/203.436/0.504 ms\n
\nIt\u2019s unclear whether this is a concern in iOS,\nbut depending on how precise the results of UIDevice
battery APIs are,\nyou may be able to use them to identify a device by its battery health.\n(Olejnik, Acar, Castelluccia, & Diaz, 2016)
var timestampedBatteryLevels : [(Date, Float)] = []\n if UIDevice.current.isBatteryMonitoringEnabled {\n timestampedBatteryLevels .append((Date(), UIDevice.current.batteryLevel ))\n }\n
\n\nEverything from your heartbeat, to your gait, to your\nbutt shape\nseem capable of leaking your identity.\nIt can all be quite overwhelming.
\nI mean,\nif a motivated individual can find your home address by\ncross-referencing the reflection in your eyes against Google Street view,\nhow can we even stand a chance out there?
\nMuch as we may bemoan the current duopoly of mobile operating systems,\nwe might take some solace in the fact that\nat least one of the players actually cares about user privacy.\nThough it\u2019s unclear whether that\u2019s a fight that can ever be won.
\nAt times,\nour fate of being tracked and advertised to\nmay seem as inevitable as the victims in It Follows.
\nBut let\u2019s not forget that,\nas technologists, as people with a voice,\nwe\u2019re in a position to fight back.
\n" }, "visual": { "url": "https://cdn.vox-cdn.com/thumbor/BkK9uk46u1ujrjGO0Jav3PCQ3g0=/0x225:5363x3800/1310x873/cdn.vox-cdn.com/uploads/chorus_image/image/59608177/PIA22227_full.0.jpg", "width": 1310, "height": 873, "contentType": "image/jpeg" }, "unread": true, "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" } ], "actionTimestamp": 1572578876975 }, { "keywords": [ "Miscellaneous" ], "originId": "https://nshipster.com/device-identifiers", "fingerprint": "2f52765d", "id": "CmHb1hXBWguYpGAhzgwJM9xvPVSYJFbt7KLqF3nqYQ0=_16e22eb0ef0:1e551:d4506071", "updated": 1572505200000, "author": "Mattt", "summary": { "direction": "ltr", "content": "For every era, there\u2019s a monster that embodies the anxieties of the age.
" }, "alternate": [ { "href": "http://feedproxy.google.com/~r/NSHipster/~3/hXb7k2avhFE/", "type": "text/html" } ], "canonical": [ { "href": "https://nshipster.com/device-identifiers/", "type": "text/html" } ], "crawled": 1572543860464, "title": "Device Identifiers and Fingerprinting on iOS", "published": 1572505200000, "origin": { "streamId": "feed/http://feeds.feedburner.com/NSHipster", "htmlUrl": "https://nshipster.com/", "title": "NSHipster" }, "content": { "direction": "ltr", "content": "For every era,\nthere\u2019s a monster that embodies the anxieties of the age.
\nAt the dawn of the Holocene,\nour ancestors traced the contours of shadows cast by the campfire\nas they kept watch over the darkness.\nOnce we learned to read the night sky for navigation,\nsailors swapped stories of sea creatures like\nLeviathan and\nSiren\nto describe the dangers of open ocean\n(and the perils to be found on unfamiliar shores).
\nFrankenstein\u2019s monster\nwas as much the creation of Mary Shelley\nas it was a spiritual collaboration with\nLuigi Galvani.\nAnd Bram Stoker\u2019s\nfictionalized account of the mummy\u2019s curse\nwas more a response to the\nEgyptomania\nand European colonialism\nof the nineteenth century\nthan any personal account of the Middle Kingdom.
\nMore recently,\nthe \u201cmonster ruins a beach party\u201d\ntrope of the 1960s\narose from concerns of teenager morality.\nWhile the\nMartians\nwho invaded those same drive-in double features\nserved as a proxy for Cold War fears at the height of the\nSpace Race.
\nAll of which begs the question:\n\u201cWhat monster best exemplifies our present age?\u201d
\nConsider the unnamed monster from the film\nIt Follows:\na formless, supernatural being that relentlessly pursues its victims\nanywhere on the planet.
\nSounds a bit like the state of\nad tech\nin 2019, no?
\n\nThis week on NSHipster \u2014\nin celebration of our favorite holiday\n\ud83c\udf83 \u2014\nwe\u2019re taking a look at the myriad ways that\nyou\u2019re being tracked on iOS,\nboth sanctioned and unsanctioned,\nhistorically and presently.\nSo gather around the campfire,\nand allow us to trace the contours of the unseen, formless monsters\nthat stalk us under cover of Dark Mode.
\nContrary to our intuitions about natural selection in the marketplace,\nhistory is littered with examples of\ninferior-but-better-marketed products winning out over superior alternatives:\nVHS vs. Betamax,\nWindows vs. Macintosh,\netc.\n(According to the common wisdom of business folks, at least.)\nRegardless,\nmost companies reach a point where\n\u201cif you build it, they will come\u201d\nceases to be a politically viable strategy,\nand someone authorizes a marketing budget.
\nMarketers are tasked with growing market share\nby identifying and communicating with as many potential customers as possible.\nAnd many \u2014\neither out of a genuine belief or formulated as a post hoc rationalization \u2014\ntake the potential benefit of their product\nas a license to flouting long-established customs of personal privacy.\nSo they enlist the help of one or more\nadvertising firms,\nwho promise to maximize their allocated budget and\nprovide some accountability for their spending\nby using technology to\ntarget,\ndeliver, and\nanalyze\nmessaging to consumers.
\nEach of these tasks is predicated on a consistent identity,\nwhich is why advertisers go to such great lengths to track you.
\nApple\u2019s provided various APIS to facilitate user identification\nfor various purposes:
\nIn the early days of iOS,\nApple provided a unique
property on UIDevice
\u2014\naffectionately referred to as a\nUDID\n(not to be confused with a UUID).\nAlthough such functionality seems unthinkable today,\nthat property existed until iOS 5,\nuntil it was\ndeprecated and replaced by identifier
in iOS 6.
Starting in iOS 6,\ndevelopers can use the\nidentifier
property on UIDevice
\nto generate a unique identifier that\u2019s shared across apps and extensions\ncreated by the same vendor\n(IDFV).
import UIKit\n let idfv = UIDevice.current.identifierForVendor // BD43813E-CFC5-4EEB-ABE2-94562A6E76CA\n
\n\nAlong with identifier
came the introduction of a new\nAdSupport framework,\nwhich Apple created to help distinguish\nidentification necessary for app functionality\nfrom anything in the service of advertising.
The resulting\nadvertisingidentifier
property\n(affectionately referred to as\nIDFA by its associates)\ndiffers from identifier
\nby returning the same value for everyone.\nThe value can change, for example,\nif the user resets their Advertising Identifier\nor erases their device.
import AdSupport \n let idfa = ASIdentifierManager .shared().advertisingIdentifier \n
\nIf advertising tracking is limited,\nthe property returns a zeroed-out UUID instead.
\nidfa.uuidString == "00000000-0000-0000-0000-000000000000" // true if the user has limited ad tracking\n
\n\n\nidentifier
and advertising
\nprovide all the same functionality as the unique
property\nthey replaced in iOS 6,\nsave for one:\nthe ability to persist across device resets and app uninstalls.
In iOS 11,\nApple quietly introduced the\nDeviceCheck framework,\nwhich allows developers to assign two bits of information\nthat are persisted by Apple\nuntil the developer manually removes them.
\nInteracting with the DeviceCheck framework should be familiar to\nanyone familiar with APNS:\nafter setting things up on App Store Connect and your servers,\nthe client generates tokens on the device,\nwhich are sent to your servers to set or query two bits of information:
\nimport DeviceCheck \n let device = DCDevice.current\n if device.isSupported {\n device.generateToken { data, error in\n if let token = data?.base64EncodedString () {\n send token to your server\n }\n }\n }\n
\nBased on the device token and other information sent by the client,\nthe server tells Apple to set each bit value\nby sending a JSON payload like this:
\n{\n"device_token": "QTk4QkFDNEItNTBDMy00Qjc5LThBRUEtMDQ5RTQzRjNGQzU0Cg==" ,\n"transaction_id": "D98BA630-E225-4A2F-AFEC-BE3A3D591708",\n"timestamp": 1572531720,\n"bit0": true,\n"bit1": false\n}\n
\nTo retrieve those two bits at a later point in time,\nthe server sends a payload without bit0
and bit1
fields:
{\n"device_token": "QTk4QkFDNEItNTBDMy00Qjc5LThBRUEtMDQ5RTQzRjNGQzU0Cg==" ,\n"transaction_id": "D98BA630-E225-4A2F-AFEC-BE3A3D591708",\n"timestamp": 1572532500\n}\n
\nIf everything worked,\nApple\u2019s servers would respond with a 200
status code\nand the following JSON payload:
{\n"bit0" : true\n"bit1" : false,\n"last_update_time" : "2019-10"\n}\n
\n\nDespite these affordances by Apple,\nadvertisers have continued to work to circumvent user privacy protections\nand use any and all information at their disposal\nto identify users by other means.
\nOver the years,\nApple\u2019s restricted access to information about\ndevice hardware,\ninstalled apps,\nnearby WiFi networks.\nThey\u2019ve required apps to request permission to\nget your current location,\naccess your camera and microphone,\nflip through your contacts, and\nfind and connect to Bluetooth accessories.\nThey\u2019ve taken bold steps to prevent user tracking in Safari.
\nFor lack of this information,\ncompanies have had to get creative,\nlooking to forge unique identities from the scraps of what\u2019s still available.\nThis process of identification by a combination of external factors\nis known as fingerprinting.
\nThe unfortunate reality is that we can be uniquely identified\nby vanishingly small amounts of information.\nFor example,\nindividuals within a population can be singled out by as few as\nfour timestamped coordinates\n(de Montjoye, Hidalgo, Verleysen, & Blondel, 2013)\nor little more than a birthday and a ZIP code\n(Sweeney, 2000).
\nEvery WWDC since 2012 has featured a session about Privacy,\nbut the only mention of fingerprinting specifically was\na brief discussion in 2014\nabout how to avoid doing it.
\nBy our count,\na determined party could use conventional, unrestricted APIs\nto generate a few dozen bits of randomness:
\nLocale information is the greatest source of identifying information\non Apple platforms.\nThe combination of your\npreferred languages, region, calendar, time zone,\nand which keyboards you have installed\nsay a lot about who you are \u2014\nespecially if you have less conventional preferences.
\nimport Foundation\n Locale.current.languageCode \n log2(Double(Locale.isoLanguageCodes .count)) // 9.217 bits\n Locale.current.regionCode \n log2(Double(Locale.isoRegionCodes .count)) // 8 bits\n Locale.current.calendar.identifier\n // ~2^4 (16) Calendars\n TimeZone .current.identifier\n log2(Double(TimeZone .knownTimeZoneIdentifiers .count)) // 8.775 bits\n UserDefaults .standard.object(forKey : "AppleKeyboards" )\n // ~2^6 (64) iOS keyboards \n
\n\nAccessibility preferences also provide a great deal of information,\nwith each individual setting contributing a single potential bit:
\nUIAccessibility.isBoldTextEnabled \n UIAccessibility.isShakeToUndoEnabled \n UIAccessibility.isReduceMotionEnabled \n UIAccessibility.isDarkerSystemColorsEnabled \n UIAccessibility.isReduceTransparencyEnabled \n UIAccessibility.isAssistiveTouchRunning \n
\nOf the approximately ~25% of users who take advantage of\nDynamic Type\nby configuring a preferred font size,\nthat selection may also be used to fingerprint you:
\nlet application = UIApplication.shared\n application.preferredContentSizeCategory \n
\nAlthough most of the juiciest bits have been locked down\nin OS updates over the years,\nthere\u2019s just enough to contribute a few more bits for purposes of identification.
\nOn iOS,\nyou can get the current model and amount of storage of a user\u2019s device:
\nimport UIKit\n let device = UIDevice.current\n device.name // "iPhone 11 Pro" \n let fileManager = FileManager .default\n if let path = fileManager .urls(for: .libraryDirectory , in: .systemDomainMask ).last?.path,\n let systemSize = try? fileManager .attributesOfFileSystem (forPath : path)[.systemSize ] as? Int\n {\n Measurement<UnitInformationStorage >(value: Double(systemSize ), unit: .bytes)\n .converted(to: .gigabytes) // ~256GB\n }\n
\nWith 14 supported iOS devices,\nmost having 3 configurations each,\nlet\u2019s say that this contributes about 32 possibilities, or 5 bits.
\nYou can go a few steps further on macOS,\nto further differentiate hardware by its processor count and amount of RAM:
\nprocessInfo .processorCount // 8\n Measurement<UnitInformationStorage >(value: Double(processInfo .physicalMemory ),\n unit: .bytes)\n .converted(to: .gigabytes) // 16GB\n
\nIt\u2019s hard to get a sense of\nhow many different Mac models are in use,\nbut a reasonable estimate would be on the order of 26 or 27.
\nKnowing whether someone\u2019s phone is on Verizon or Vodaphone\ncan also be factored into a fingerprint.\nYou can use the CTTelephony
class from the\nCoreTelephony framework\nto lookup the providers for devices with cellular service:
import CoreTelephony \n let networkInfo = CTTelephonyNetworkInfo ()\n let carriers = networkInfo .serviceSubscriberCellularProviders ?.values\n carriers?.map { ($0.mobileNetworkCode , $0.mobileCountryCode ) }\n
\nThe number of providers varies per country,\nbut using the 4 major carriers in United States\nas a guideline,\nwe can say carrier information would contribute about 2 bits\n(or more if you have multiple SIM cards installed).
\nMore generally,\neven knowing whether someone can send texts or email at all\ncan be factored into a fingerprint.\nThis information can be gathered without permissions via\nthe MessageUI framework.
\nimport MessageUI \n MFMailComposeViewController .canSendMail ()\n MFMessageComposeViewController .canSendText ()\n
\nIf the use of digital fingerprinting seems outlandish,\nthat\u2019s just scratching the surface of how companies and researchers\nhave figured out how to circumvent your privacy.
\nAlthough access to geolocation through conventional APIs\nrequires explicit authorization,\nthird parties may be able to get a general sense of where you are in the world\nbased on how you access the Internet.
\nGeolocation by source IP address\nis used extensively for things like region locking and localization.\nYou could also combine this information with\nping-time measurements\nto hosts in known locations\nto get a more accurate pinpoint on location (Weinberg, Cho, Christin, Sekar, & Gill, 2018):
\nping -c 5 99.24.18.13 # San Francisco, USA\n \n--- 99.24.18.13 ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 11.900/12.184/12.895/0.380 ms\nping -c 5 203.47.10.37 # Adelaide, Australia\n \n--- 203.47.10.37 ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 202.122/202.433/203.436/0.504 ms\n
\nIt\u2019s unclear whether this is a concern in iOS,\nbut depending on how precise the results of UIDevice
battery APIs are,\nyou may be able to use them to identify a device by its battery health.\n(Olejnik, Acar, Castelluccia, & Diaz, 2016)
var timestampedBatteryLevels : [(Date, Float)] = []\n if UIDevice.current.isBatteryMonitoringEnabled {\n timestampedBatteryLevels .append((Date(), UIDevice.current.batteryLevel ))\n }\n
\n\nEverything from your heartbeat, to your gait, to your\nbutt shape\nseem capable of leaking your identity.\nIt can all be quite overwhelming.
\nI mean,\nif a motivated individual can find your home address by\ncross-referencing the reflection in your eyes against Google Street view,\nhow can we even stand a chance out there?
\nMuch as we may bemoan the current duopoly of mobile operating systems,\nwe might take some solace in the fact that\nat least one of the players actually cares about user privacy.\nThough it\u2019s unclear whether that\u2019s a fight that can ever be won.
\nAt times,\nour fate of being tracked and advertised to\nmay seem as inevitable as the victims in It Follows.
\nBut let\u2019s not forget that,\nas technologists, as people with a voice,\nwe\u2019re in a position to fight back.
\n\n" }, "visual": { "url": "http://www.blogcdn.com/www.engadget.com/media/2013/10/irlbanner-1382819058.jpg", "width": 620, "height": 194, "contentType": "image/jpeg" }, "unread": false, "readTime": 5062, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366", "label": "Weblogs" } ], "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": 1572578867134 }, { "originId": "https://inessential.com/2019/10/31/amateurs", "fingerprint": "a55a55f8", "id": "+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16e23877a2b:1e9e9:d4506071", "summary": { "direction": "ltr", "content": "One thing I\u2019m weirdly proud of is my position as an amateur programmer.
\nWhen I point that out, people say, \u201cWell, but\u2026\u201d \u2014\u00a0and I know where they\u2019re going, that after 25 years of professional experience I\u2019m not what you think of when you think of \u201camateur.\u201d
\nAnd yet, it\u2019s still true. It\u2019s just that I\u2019ve come out the other side, and now I get to work on exactly what I want to, the way I want to, without any thoughts of trying to make money at it.
\nI can take risks! I can work with anybody who shows up! It\u2019s a pure thrill. It\u2019s like writing single-malt apps.
\nAnd I would wish for more people to find themselves in this position \u2014 eventually, anyway \u2014\u00a0because I want to see what they would make.
\nPS The Dictionary app on my Mac says of the origin of the word \u201camateur\u201d:
\n\n\nlate 18th century: from French, from Italian amatore, from Latin amator \u2018lover\u2019, from amare \u2018to love\u2019.
\n
Spot-on.
" }, "alternate": [ { "href": "https://inessential.com/2019/10/31/amateurs", "type": "text/html" } ], "crawled": 1572554111531, "title": "Amateurs", "published": 1572553200000, "origin": { "streamId": "feed/http://ranchero.com/xml/rss.xml", "htmlUrl": "https://inessential.com/", "title": "inessential.com" }, "visual": { "url": "none" }, "unread": true, "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" } ], "actionTimestamp": 1572578844326 }, { "originId": "tag:daringfireball.net,2019:/linked//6.36212", "fingerprint": "9dc675e8", "id": "ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16e23dce615:1ec09:d4506071", "updated": 1572557257000, "author": "John Gruber", "alternate": [ { "href": "https://twitter.com/jack/status/1189634360472829952", "type": "text/html" } ], "crawled": 1572559709717, "title": "Twitter to Stop Accepting Political Ads Globally", "published": 1572557256000, "origin": { "streamId": "feed/http://daringfireball.net/index.xml", "htmlUrl": "https://daringfireball.net/", "title": "Daring Fireball" }, "content": { "direction": "ltr", "content": "Jack Dorsey, in a tweet thread:
\n\n\nFor instance, it\u2018s not credible for us to say: \u201cWe\u2019re working hard\nto stop people from gaming our systems to spread misleading info,\nbuuut if someone pays us to target and force people to see their\npolitical ad\u2026 well\u2026 they can say whatever they want!\u201d [\u2026]
\nThis isn\u2019t about free expression. This is about paying for reach.\nAnd paying to increase the reach of political speech has\nsignificant ramifications that today\u2019s democratic infrastructure\nmay not be prepared to handle. It\u2019s worth stepping back in order\nto address.
\n
Political advertising is a drop in the bucket of Twitter\u2019s overall revenue, but that\u2019s true of Facebook too. \u201cThe money matters to us\u201d would be a terrible justification for Facebook\u2019s policy of allowing political ads to spread falsehoods, but the money doesn\u2019t even matter to them. Facebook is allowing political ads to spread falsehoods because Facebook wants political ads to spread falsehoods. There\u2019s no other explanation.
\n" }, "unread": true, "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" } ], "actionTimestamp": 1572578825713 }, { "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.
\n\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\n" }, "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": 4744, "categories": [ { "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366", "label": "Weblogs" } ], "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": 1572500250190 }, { "originId": "https://inessential.com/2019/10/22/netnewswire_5_0_3_for_mac_released", "fingerprint": "2c4d6d88", "id": "+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16df54dfcca:13322:d4506071", "summary": { "direction": "ltr", "content": "\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.
\n\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.
\n\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:
\n\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.
\n\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": "\nSonos 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": "\nRumors 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": "