Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Is concurrency worth the effort?

Is concurrency worth the effort?

Lessons learned updating project with ~100k lines of code, from closures and Swift 5 to full-on concurrency and Swift 6.

Presented at Pragma Conference 2024.

More Decks by Aleksandar Vacić (Radiant Tap)

Other Decks in Programming

Transcript

  1. “I'm f a irly cert a in no existing l

    a rge size codeb a se will ever be a ble to fully a dopt Swift Concurrency Checking a nd Swift 6. I would a lso expect a short-term rise in concurrency bugs in iOS a pps.”
  2. “I'm f a irly cert a in no existing l

    a rge size codeb a se will ever be a ble to fully a dopt Swift Concurrency Checking a nd Swift 6. I would a lso expect a short-term rise in concurrency bugs in iOS a pps.” “M a n, Swift 6 will be merciless on existing codeb a ses. I a dvise you to a dd -strict-concurrency=complete to your current Swift project a nd st a rt f ixing a w a y. Be prep a red.”
  3. “I'm f a I would a “M a n, Swift

    6 will be merciless on existing codeb a ses. I a dvise you to a dd -strict-concurrency=complete to your current Swift project a nd st a rt f ixing a w a y. Be prep a red.” “ I'm confused a bout concurrency a g a in, c a n you ple a se help?”
  4. “I'm f a I would a “M a Be prep

    a “ I'm confused a bout concurrency a g a in, c a n you ple a se help?” “But is it something we need to be pushing on a ll iOS a pp developers? I know it’s option a l, but m a ny will not see it th a t w a y a nd will try to do everything they c a n to get their code b a se to Swift 6, even if th a t’s a t the cost of re a d a bility.”
  5. “I'm f a I would a “M a Be prep

    a “ I'm confused a “But is it something we need to be pushing on a ll iOS a pp developers? I know it’s option a l, but m a ny will not see it th a t w a y a nd will try to do everything they c a n to get their code b a se to Swift 6, even if th a t’s a t the cost of re a d a bility.” “Swift 6 concurrency? Good luck ”
  6. /Sep 2023/ Project state • iOS 15 minimum deployment version.

    • 100% Swift 5.5+ code. • All UIKit + storybo a rds, using KiLS a rchitecture. • Core D a t a store. • 3 m a in b a ckend services sh a ring one OAuth 2 b a sed a uthoriz a tion service. • 8 a ddition a l d a t a sources which integr a te with the m a in 3. • Custom on-the- f ly tr a nsl a tion engine. • 17 extern a l dependencies, a ll SPM. • ~80k lines of Swift code • ~30k lines of XML • ~15k lines of JSON • ~8k lines of v a rious other a ssets (SVG, CSS, HTML etc)
  7. “It’s import a nt a t this point not to

    p a nic” – Migrate your app to Swift 6 · WWDC24
  8. “Closures! Closures, everywhere.” Pre-concurrency iOS developer, thinking the only work

    is to replace closures with async/await. Instead it is… …they are all going away?
  9. Type 'FieldModel' does not conform to the 'Send a ble'

    protocol; this is a n error in the Swift 6 l a ngu a ge mode Sending 'model' risks c a using d a t a r a ces; this is a n error in the Swift 6 l a ngu a ge mode M a in a ctor-isol a ted property 'textContentType' c a n not be mut a ted from a nonisol a ted context; this is a n error in the Swift 6 l a ngu a ge mode C a ll to m a in a ctor-isol a ted inst a nce method 'register(_:withReuseIdenti f ier:)' in a synchronous nonisol a ted context; this is a n error in the Swift 6 l a ngu a ge mode C a pture of 'bestAttemptContent' with non-send a ble type 'UNMut a bleNoti f ic a tionContent' in a `@Send a ble` closure; this is a n error in the Swift 6 l a ngu a ge mode St a tic property 'utcD a teTimeForm a tter' is not concurrency-s a fe bec a use it is nonisol a ted glob a l sh a red mut a ble st a te; this is a n error in the Swift 6 l a ngu a ge mode Type 'CurrentTicketD a t a Source.GridItem' does not conform to the 'Send a ble' protocol; this is a n error in the Swift 6 l a ngu a ge mode
  10. “The other thing to remember is you don’t h a

    ve to a ddress a ll these issues in one sitting. If you h a ve to ship a rele a se, or go work on some more pressing ch a nge, you c a n go b a ck into Settings a nd turn strict checking b a ck o ff . All the ch a nges you m a de to reduce those w a rnings will be v a lid improvements to the code b a se th a t you c a n keep a nd check in, even if you then go b a ck to minim a l checking for a while. You c a n return to [strict checking] l a ter when you’re re a dy, a nd t a ckle them then.” – Migrate your app to Swift 6 · WWDC24
  11. import PackageDescription let package = Package( name: "Notifying", products: [

    .library( name: "Notifying", targets: ["Notifying"] ) ], targets: [ .target( name: "Notifying" ), .testTarget( name: "NotifyingTests", dependencies: ["Notifying"] ) ] )
  12. import PackageDescription let package = Package( name: "Notifying", products: [

    .library( name: "Notifying", targets: ["Notifying"] ) ], targets: [ .target( name: "Notifying", swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") ] ), .testTarget( name: "NotifyingTests", dependencies: ["Notifying"] ) ] )
  13. Sprinkle Sendable on e a ch model type you h

    a ve. Every one. Compiler will then immedi a tely tell you if some of those model types c a n’t be send a ble a nd why, which is a n opportunity to think-through the us a ge of th a t speci f ic model type.
  14. Del a y a dding @unchecked Sendable until the very

    end of the tr a nsition. The w a rnings will keep bugging you but over time, a s you upd a te the a pp, you h a ve a gre a ter ch a nce to a void @unchecked entirely.
  15. Never use actor for model types. Don’t even think a

    bout it. If you end in a situ a tion where your model type should potenti a lly be a n a ctor you a re likely m a king a serious a rchitectur a l / design p a ttern mist a ke.
  16. actor is nothing more th a n a class with

    priv a te seri a l queue. It m a kes code more re a d a ble but does not s a ve you from r a ce conditions. Reentr a ncy is very re a l a nd super h a rd problem.
  17. D a t a f low involving a ch a

    in of methods where a t le a st one method is async ⇥ convert entire ch a in to be async except the f irst method in the ch a in. - In other words: keep the us a ge of Task{} a t minimum.
  18. Challenges It took < 2 months to do 90%. Then

    5 more to cle a r up rem a ining 10%. Why?
  19. Some observ a tions on Swift concurrency: • It h

    a s c a ught a bunch of mist a kes I m a de. • I m a de a couple of mist a kes trying to speed run f ixes. • Some of the solutions to the problems a re h a rder to gr a sp th a n I hoped. • Wish there w a s a “sure you a re correct but I c a n gu a r a ntee this a in’t ever going to h a ppen, let me do it” – Miguel de Icaza
  20. func MBLocalizedString(_ key: String, comment: String) -> String { return

    TranslationManager.translation(for: key) } On-the- fl y translations
  21. public class SettingsStore { open var languageCode: String? open var

    MBLocalizedString: (_ key: String, _ comment: String) -> String = { key, _ in return key } } Quick & shoddy solution for tr a nsl a tions…
  22. private(set) lazy var settings: SettingsStore = { let svc =

    SettingsStore(…) // pass the app's translator to the module svc.MBLocalizedString = { MBLocalizedString($0, comment: $1) } svc.languageCode = Locale.app.languageCode return svc }() public class SettingsStore { open var languageCode: String? open var MBLocalizedString: (_ key: String, _ comment: String) -> String = { key, _ in return key } } …inside modules
  23. • fetch tr a nsl a tions in the m

    a in a pp a nd s a ve them loc a lly • cre a te Tr a nsl a tions p a ck a ge th a t re a ds th a t loc a l f ile • a lso exposes public MBLocalizedString() method • a dd Tr a nsl a tions a s dependency to a ll other p a ck a ges a nd the m a in a pp Proper solution
  24. Task { [weak self] in guard let self else {

    return } let delay: TimeInterval = 5 try await Task.sleep(nanoseconds: delay.nanoseconds) self.toggleSearchButton(enabled: true) } Nightmare fodder
  25. Task { [weak self] in let delay: TimeInterval = 5

    try await Task.sleep(nanoseconds: delay.nanoseconds) guard let self else { return } self.toggleSearchButton(enabled: true) } Nightmare fodder
  26. When upd a ting UI, never mix rendering, a w

    a iting, rendering a bit more, a w a iting something else… Alw a ys f irst a w a it a ll the d a t a you need a nd then st a rt rendering in one uninterrupted sequence of synchronous code. Nightmare fodder
  27. When upd a Alw a func populateSummariesStack() async { summariesStackView.removeAllSubviews()

    guard let dataSource = dataSource else { return } let summaryItems = await dataSource.summaryItems for item in summaryItems { let pair = TicketSummaryPairView.nibInstance pair.translatesAutoresizingMaskIntoConstraints = false pair.populate(with: item) summariesStackView.addArrangedSubview(pair) } }
  28. Alw a func populateSummariesStack() async { guard let dataSource =

    dataSource else { return } let summaryItems = await dataSource.summaryItems summariesStackView.removeAllSubviews() for item in summaryItems { let pair = TicketSummaryPairView.nibInstance pair.translatesAutoresizingMaskIntoConstraints = false pair.populate(with: item) summariesStackView.addArrangedSubview(pair) } }
  29. Every single await a nd Task you write incre a

    ses prob a bility for r a ce conditions. Full-on concurrency will cert a inly reve a l even the sm a llest logic a l errors, prem a ture f l a g checks a nd toggles etc. Nightmare fodder
  30. func postLogin(andAnnounce notify: Bool = false, flowIdentifier fid: String) async

    { do { // fetch account state try await fetchCurrentAccount(flowIdentifier: fid) // fetch active promotion progress try await fetchActivePromotionProgress(flowIdentifier: fid) // fetch KPI try await fetchAccountKPI(andAnnounce: notify, flowIdentifier: fid) // check if there are player limits setup and possibly reached await postLoginPlayerLimitsCheck(flowIdentifier: fid) // setup session limit if there is one configured on this account setupSessionTimer(flowIdentifier: fid) // do we have any pending notifications (done here to update badge on profile icon) try await fetchPendingAtlasNotifications(flowIdentifier: fid) try await fetchAtlasNotificationsHistory(flowIdentifier: fid) // continuous periodical update setupAtlasNotificationsTimer() // continuous periodical update setupBalanceTimer() } catch let err { log(level: .warning, flowIdentifier: fid, String(reflecting: err)) } } Horribly wrong piece of code
  31. func postLogin(andAnnounce notify: Bool = false, flowIdentifier fid: String) async

    { // fetch account state do { try await fetchCurrentAccount(flowIdentifier: fid) } catch let err { log(level: .warning, flowIdentifier: fid, String(reflecting: err)) } // fetch active promotion progress do { try await fetchActivePromotionProgress(flowIdentifier: fid) } catch let err { log(level: .warning, flowIdentifier: fid, String(reflecting: err)) } // fetch KPI do { try await fetchAccountKPI(andAnnounce: notify, flowIdentifier: fid) } catch let err { log(level: .warning, flowIdentifier: fid, String(reflecting: err)) } … }
  32. I kind of regret not w a iting a little

    longer. Swift 6 obsoleted quite a bit of work th a t w a s necess a ry only for 5.10. I tend to think of this rele a se more like Concurrency 1.0. – Matthew Massicotte
  33. override func awakeFromNib() { super.awakeFromNib() cleanup() applyTheme() textField.isUserInteractionEnabled = false

    textField.addTarget(self, action: #selector(editingChanged), for: .editingChanged) } ⇤ this is not UIKit method 🧐
  34. Th a Mr. M a override func awakeFromNib() { super.awakeFromNib()

    MainActor.assumeIsolated { cleanup() applyTheme() textField.isUserInteractionEnabled = false textField.addTarget(self, action: #selector(editingChanged), for: .editingChanged) } }
  35. • It provided enough incentive for modul a riz a

    tion, which w a s by f a r the most time consuming p a rt. • It m a de the code w a y more re a d a ble a nd extend a ble. • It forced us to re-think a nd re-do m a ny h a cks a nd question a ble shortcuts we h a d in the code. • Eventu a lly m a de the a pp more st a ble.
  36. I m a int a in a quite big codeb

    a se a nd is f ine. We en a ble it in the modules we c a n (th a t we were re a dy for, during Swift 5 d a ys which is gre a t), on some is en a bled but with a couple of uns a fe a nd the rest is in 5 a nd f ine. I feel like people a re being overdr a m a tic a bout Swift 6 concurrency. Is a n option a l mode, it c a n be en a bled by module a nd there a re uns a fe opt outs. I honestly think the Swift te a m went f a r a nd beyond to m a ke this a good tr a nsition. Is it perfect? No. But I don't see the dr a m a . – Alejandro Martinez