{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","items":[{"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 — making the best app we can — we 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’t 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’s 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 — where 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’t finished yet) — and we’ll make that version available indefinitely for people who haven’t upgraded to Catalina.
\nThis will mean that people running older OSes will still get a high-quality app — it’s just that it won’t 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’ll understand why we decided to do it this way. Decisions like this are never easy — there are always conflicting values to weigh, pros and cons and add up — and we don’t 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.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571123677415},{"keywords":["Xcode"],"originId":"https://nshipster.com/swiftui-previews","fingerprint":"e3a6f78b","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 — and it’s 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…
\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’t, of course,\nso you jump back into Xcode,\ntweak the Content Hugging Priority,\nhit ⌘R,\nand start the whole process again.
\nWe might relate our sorry predicament to\nthat one xkcd comic,\nbut for those of us who don’t so much relish in\nthe stop-and-go nature of app development,\nthere’s an old Yiddish joke about Shlemiel the painter\n(provided below with a few -specific modifications;\nfor the uninitiated,\nplease refer to Joel Spolsky’s\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“That’s pretty good!” says his manager,\n“you’re a fast worker!” and pays him a Bitcoin.
\nThe next sprint Shlemiel only gets 5 screens done.\n“Well, that’s not nearly as good as yesterday,\nbut you’re still a fast worker. 5 screens is respectable,”\nand pays him a Bitcoin.
\nThe next sprint Shlemiel implements 1 screen.\n“Only 1!” shouts his manager.\n“That’s unacceptable!\nOn the first day you did ten times that much work!\nWhat’s going on?”
\n“I can’t help it,” says Shlemiel.\n“Each sprint I get further and further away from\n
\napplication(_:did
!”Finish Launching With Options:)
Over the years,\nthere have been some developments that’ve helped things slightly,\nincluding\n@IBInspectable
and @IBDesignable
\nand Xcode Playgrounds.\nBut with Xcode 11,\nour wait is finally over —\nand it’s all thanks to SwiftUI.
Although many of us have taken a “wait and see” approach to SwiftUI,\nwe can start using its capabilities today\nto radically speed up and improve our development process —\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’d 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 —\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’s say our app had a Favorite
—\na distant cousin (perhaps by composition) to Bordered
.\nIn its default state,\nit shows has the title “Favorite”\nand displays a ♡ icon.\nWhen its is
property is set to true
,\nthe title is set to “Unfavorite”\nand displays a ♡̸ 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’s 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’s 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", // Arabid (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’t 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 —\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 “nice to have,”\nthey’re 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’s hard to overstate how much of a game-changer Xcode Previews are for iOS development,\nand we couldn’t 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":"