Slide 1

Slide 1 text

ࠓ೔͔Β࢖͑Δ࣮ફతSwift Concurrency @giginet 1

Slide 2

Slide 2 text

ࣗݾ঺հ • @giginet • Core Contributor of Carthage/fastlane/ XcodeGen etc... • https://github.com/giginet • https://twitter.com/giginet • ΫοΫύου(2015/4~) ϞόΠϧج൫ ෦ • ؾܰʹmention͍ͯͩ͘͠͞ʂ 2

Slide 3

Slide 3 text

iOSDC ࿩͠·ͨ͠ େن໛ͳΞϓϦͷϚϧνϞδϡʔϧߏ੒ͷ࣮ફ by giginet | τʔΫ | iOSDC Japan 2021 #iosdc - fortee.jp 3

Slide 4

Slide 4 text

ٕज़ސ໰giginet͞ΜΠϯλϏϡʔ ίϛϡχςΟ׆ಈ΍OSS׆ಈͷ ൿ݃Λฉ͍ͯΈ·ͨ͠ | Money Forward Engineers' Blog 4

Slide 5

Slide 5 text

Agenda • Swift Concurrency·ͱΊ • جຊฤ • Structured Concurrency • actor 5

Slide 6

Slide 6 text

• ࣮ફతSwift Concurrency • طଘͷΞϓϦͷҠߦ • Α͋͘Δ࣮૷ΛConcurrencyԽͯ͠ΈΑ͏ • callbackύλʔϯ • Delegateύλʔϯ • RxSwiftͱͷڠௐ • σΟεΧογϣϯ 6

Slide 7

Slide 7 text

Swift Concurrency·ͱΊ ͍Ζ͍Ζࢿྉ͕͋ΔͷͰجຊ͸ͦͪΒΛݟΕ͹ྑ͍ɻࠓճ͸๩͠ ͍ํ޲͚ʹ͜ΕΛϕʔεʹ؆୯ʹઆ໌͍͖ͯ͠·͢ɻ • WWDC 2021ͷSwiftͷฒߦॲཧؔ࿈ͷηογϣϯҰཡ - Qiita • async/await΍actorͰiOSΞϓϦ։ൃ͕Ͳ͏มΘΔ͔ Before & Afterͷ۩ମྫͰֶͿ - Speaker Deck • Swift Concurrency νʔτγʔτ 7

Slide 8

Slide 8 text

Swift Concurrencyجຊฤ 8

Slide 9

Slide 9 text

• Α͋͘Δ΍ͭɻcallbackΛड͚औͬͯResultͰ΋Β͏ Before func downloadData(from url: URL, completion:@escaping (Result) -> Void) downloadData(from: url) { result in switch result { case .success(let data): // data Λ࢖͏ॲཧ case .failure(let error): // ΤϥʔϋϯυϦϯά } } 9

Slide 10

Slide 10 text

After func downloadData(from url: URL) async throws -> Data do { let data = try await downloadData(from: url) // data Λ࢖͏ॲཧ } catch { // ΤϥʔϋϯυϦϯά } 10

Slide 11

Slide 11 text

ྫ2 • ·ͣϢʔβʔσʔλͷJSONΛ΋ΒͬͯɺͦͷJSONΛσίʔ υɺͦͷதʹ͋ΔURL͔Βը૾Λऔಘ͢Δ 11

Slide 12

Slide 12 text

func fetchUserIcon(for id: User.ID, completion: @escaping (Result) -> Void) { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! downloadData(from: url) { data in // (1) do { let data = try data.get() let user = try JSONDecoder().decode(User.self, from: data) downloadData(from: user.iconURL) { icon in // (2) do { let icon = try icon.get() completion(.success(icon)) } catch { completion(.failure(error)) } } } catch { completion(.failure(error)) } } } 12

Slide 13

Slide 13 text

After func fetchUserIcon(for id: User.ID) async throws -> Data { let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! let data = try await downloadData(from: url) let user = try JSONDecoder().decode(User.self, from: data) let icon = try await downloadData(from: user.iconURL) return icon } • Θ͔Γ΍͍͢ • खଓ͖తʹॻ͚Δ 13

Slide 14

Slide 14 text

ྫ3 • JSONͷதʹ2ͭͷը૾URLؚ͕·Ε͍ͯΔ(smallIconImage, largeIconImage) • ·ͣJSONΛऔಘ͠ɺؚ·ΕΔը૾Λฒߦ(Parallel)ͯ͠μ΢ϯ ϩʔυ͍ͨ͠ 14

Slide 15

Slide 15 text

• JSONͷऔಘޙʹը૾ͷऔಘʢґଘؔ܎ˠίʔϧόοΫͷ࿈࠯ʣ • 2ͭͷը૾͸Parallelʹऔಘ͢Δඞཁ͕͋Δ • GCDͷDispatchGroupͳͲΛར༻ 15

Slide 16

Slide 16 text

! func fetchUserIcons(for id: User.ID, completion: @escaping (Result<(small: Data, large: Data), Error>) -> Void) { downloadData(from: url) { data in // (1) do { let data = try data.get() let user = try JSONDecoder().decode(User.self, from: data) let group: DispatchGroup = .init() var smallIcon: Result! group.enter() downloadData(from: user.smallURL) { icon in smallIcon = icon group.leave() } var largeIcon: Result! group.enter() downloadData(from: user.largeURL) { icon in largeIcon = icon group.leave() } group.notify(queue: .global()) { do { let icons = try (small: smallIcon.get(), large: largeIcon.get()) completion(.success(icons)) } catch { completion(.failure(error)) } } } catch { completion(.failure(error)) } } } 16

Slide 17

Slide 17 text

After let url: URL = .init(string: "https://koherent.org/fake-service/api/user?id=\(id)")! let data = try await downloadData(from: url) let user = try JSONDecoder().decode(User.self, from: data) async let smallIcon = try await downloadData(from: user.smallURL) async let largeIcon = try await downloadData(from: user.largeURL) let icons = try await (small: smallIcon, large: largeIcon) 17

Slide 18

Slide 18 text

Structured Concurrency • ෳ਺ͷasyncॲཧΛߏ଄Խ࣮ͯ͠ߦ͢Δ࢓૊Έ • Task • ্هͷྫͷΑ͏ʹෳ਺ͷλεΫΛฒߦͯ͠ߦͬͨΓ • ࢠλεΫΛ࡞ͬͨΓ • Ωϟϯηϧͨ͠Γ • ͕ՄೳʹͳΔ 18

Slide 19

Slide 19 text

• swift-evolution/0304-structured-concurrency.md at main · apple/swift-evolution • Explore structured concurrency in Swift - WWDC21 - Videos - Apple Developer 19

Slide 20

Slide 20 text

async-let • 1ͭͷTask಺Ͱෳ਺ͷඇಉظॲཧ(ࢠλεΫ)Λ૸Βͤͯ଴ͪड͚ Ͱ͖Δ async let smallIcon = try await downloadData(from: user.smallIconURL) // (foo) async let largeIcon = try await downloadData(from: user.largeIconURL) // (bar) // ͳΜ͔௕͍ॲཧ (Task) let icons = try await (small: smallIcon, large: largeIcon) 20

Slide 21

Slide 21 text

21

Slide 22

Slide 22 text

Task • ඇಉظؔ਺͸TaskͰϥοϓ͢Δ͜ͱͰಉظͷੈք͔ΒͰ΋࣮ߦ Ͱ͖Δ • ಉظੈքͱඇಉظੈքͷڮ౉͠ʹ࢖͑Δ func syncMethod() { Task { await doSomething() } } 22

Slide 23

Slide 23 text

Cancel func syncMethod() { let task = Task { await doSomething() } task.cancel() } • Task͸cancelͰ͖Δ • ϘλϯΛԡͨ͠ΒTaskΛൃՐ͢Δ͕ɺऴΘΔલʹΩϟϯηϧϘ λϯΛԡͨ͠ΒऴྃΈ͍ͨͳͷ͕ग़དྷΔ • ࢠλεΫ΋·ͱΊͯΩϟϯηϧ͞ΕΔ 23

Slide 24

Slide 24 text

AsyncSequence • asyncʹ஋͕औΕΔiterator • ϑΝΠϧI/Oͱ͔ • streamͱ͔(WebSocketͱ͔) • ࢠλεΫΛಈతʹ૿΍ͯ͠ɺiteratorͷΑ͏ʹѻ͑Δ Meet AsyncSequence - WWDC21 - Videos - Apple Developer 24

Slide 25

Slide 25 text

actor • ඇಉظͷσʔλڝ߹Λղܾ͢Δ͘͠Έ • ॲཧΛඇಉظʹͨ͠ΓɺΩϡʔΛ੍໿ͨ͠ΓɺϩοΫͷऔಘΛ ࣗಈతʹ΍ͬͯ͘ΕΔ 25

Slide 26

Slide 26 text

σʔλڝ߹ͱ͸ • ҎԼͷ৔߹ɺյΕͯ͠·͏ let counter: Counter = .init() DispatchQueue.global().async { print(counter.increment()) // ? } DispatchQueue.global().async { print(counter.increment()) // ? } 26

Slide 27

Slide 27 text

actor Counter { private var count: Int = 0 func increment() -> Int { count += 1 return count } } 27

Slide 28

Slide 28 text

• actor಺ͷશͯͷϓϩύςΟ΍ϝιου͕ඇಉظʹͳΔͨΊɺ σʔλڝ߹͔ΒकΒΕΔ • actor͸಺෦ʹઐ༻ͷΩϡʔ(serial executor)Λอ͓࣋ͯ͠ΓɺӅ ṭ͞ΕΔ await counter.increment() 28

Slide 29

Slide 29 text

• GCDͰ࣮ݱ͠Α͏ͱ͢ΔͱɺϩοΫ΍semaphore͕ඞཁʹͳΔ final class Counter { private let queue: DispatchQueue = .init(label: ...) private var count: Int = 0 func increment(completion: @escaping (Int) -> Void) { queue.async { [self] in count += 1 completion(count) } } } 29

Slide 30

Slide 30 text

Global Actor • actorͷ࢖༻ΩϡʔΛಛఆͷΩϡʔʹ੍ݶ͢ΔͨΊͷ͘͠Έ • SEͰ͸೚ҙΩϡʔ͕ఏҊ͞Ε͍ͯΔ͕ɺݱࡏ͸MainActorͷ Έ 30

Slide 31

Slide 31 text

@MainActor final class MyViewController: UIViewController { func doSomething() { } } • actorͰ͸ͳ͘ɺطଘͷΫϥεʹ@MainActorΛ෇͚Δͱϝϯό͕ શ෦asyncʹͳΓϝΠϯεϨουͰ࣮ߦ͞ΕΔ 31

Slide 32

Slide 32 text

• ֎͔Β͸ϝιου΍ϓϩύςΟ͕asyncʹݟ͑Δ • MainActorʹ͍Δ΋ͷͷத͔Β͸͔͋ͨ΋ಉظతʹݺ΂Δ • MainActorҎ֎͔Βͷ࣮ߦΛ͋Δఔ౓๷͙͜ͱ͕ग़དྷΔ • UIͷߋ৽ͳͲɺϝΠϯεϨουҎ֎Ͱ࣮ߦ͞Εͯཉ͘͠ͳ͍ ΋ͷΛؒҧͬͯݺ͹ͳ͍Α͏ʹίϯύΠϧ࣌ʹอޢ͢Δ͜ͱ ͕Ͱ͖Δ 32

Slide 33

Slide 33 text

@MainActor func reloadData() { // ϝΠϯεϨουͰ࣮ߦͯ͠ཉ͍͠ॲཧ } @MainActor final class SomeViewController: UIViewController { override func viewDidLoad() { reloadData() } } func callReloadData() { let vc = SomeViewController() vc.reloadData() // ϝΠϯεϨουอূ͕ͳ͍ͷͰasync͡Όͳ͍ͱݺ΂ͳ͍ } 33

Slide 34

Slide 34 text

UIViewController • υΩϡϝϯτΛݟΔײ͡ɺUIViewController͸શͯ@MainActor ʹͳ͍ͬͯΔ • https://developer.apple.com/documentation/uikit/ uiviewcontroller?changes=latest_minor • ࣮ࡍʹ͜ΕΛ΍Δͱଟ͘ͷϓϩμΫτͰޓ׵ੑ͕ҡ࣋Ͱ͖ͳ͘ ͳΔΑͳͱࢥ͍ͬͯΔɻͳΜ͔Έͨײ͡΋దԠ͞Εͯແͦ͞͏ ͰΑ͘Θ͔Βͳ͍ ! 34

Slide 35

Slide 35 text

35

Slide 36

Slide 36 text

class User { var name: String var age: Int } actor Session { var currentUser: User? } 36

Slide 37

Slide 37 text

func run() async { let session = Session() let user = await session.currentUser user?.age += 1 } 37

Slide 38

Slide 38 text

38

Slide 39

Slide 39 text

ͳͥʁ • User͸ࢀরܕͰࢀরઌͰ஋͕มΘΓ͏Δ • ෳ਺ͷλεΫͰࢀর͍ͯ͠Δؒʹ1೥ܦͬͯage͕૿͑ΔՄೳੑ ͕͋Δ • σʔλڝ߹ͷةݥੑ͕͋Δ 39

Slide 40

Slide 40 text

Sendable • ͜ͷܕ͕actorؒͰ΍ΓͱΓͰ͖Δ͜ͱΛprotocolʹ௨஌͢Δ protocol(marker protocol) • swift-evolution/0302-concurrent-value-and-concurrent- closures.md at main · apple/swift-evolution 40

Slide 41

Slide 41 text

class User: Sendable { let user: String let age: Int } • Sendableʹద߹͢Δ͜ͱͰίϯύΠϥʹ҆શʹड͚౉͠Ͱ͖Δ ܕͰ͋Δ͜ͱΛ௨஌ • ্هͷΑ͏ͳΠϛϡʔλϒϧΫϥε(varΛ࣋ͨͣʹҰ౓࡞ͬͨ ΒมΘΒͳ͍)͸SendableʹͰ͖Δ 41

Slide 42

Slide 42 text

• ͜ͷܯࠂ͸Xcode 13.0(Swift 5.5)ͷ࣌఺Ͱ͸ແޮʹͳ͍ͬͯΔ (؇΍͔ͳҠߦͷͨΊ) • Swift 6.0͔Βܯࠂ͕ඪ४Ͱग़ΔΑ͏ʹͳΔͷͰিܸʹඋ͑Δ • -warn-concurencyΦϓγϣϯͰܯࠂΛ༗ޮԽͰ͖Δ 42

Slide 43

Slide 43 text

• Swift ConcurrencyͷwithTaskCancellationHandlerͱSendable - cockscomblog? • Concurrency in Swift 5 and 6 - Evolution / Discussion - Swift Forums 43

Slide 44

Slide 44 text

Concurrencyͷར఺ • ॲཧͷߏ଄Խ • ॲཧͷྲྀΕΛಡΈԼͤͯ௥͍΍͍͢ • ݕࠪྫ֎ͷੵۃతͳ࢖༻ • σʔλڝ߹ͷίϯύΠϧ࣌νΣοΫ • ࣮ߦεϨουͷίϯύΠϧ࣌νΣοΫ 44

Slide 45

Slide 45 text

Special Thanks • ͜͜·ͰͷεϥΠυͷ΄ͱΜͲ͸ @koher ͞Μͷࢿྉͷ௨ΓͰ ͢ ! • ૉ੖Β͍͠ͷͰαϙʔτ͠·ͨ͠ɻ͋Γ͕ͱ͏͍͟͝·͢ 45

Slide 46

Slide 46 text

໌೔͔Β࢖͑ΔSwift Concurrency • طଘͷΞϓϦΛ෦෼తʹSwift ConcurrencyରԠ͍ͯ͘͠ςΫ χοΫ 46

Slide 47

Slide 47 text

Swift Concurrency backport • ݱࡏɺConcurrency͸iOS 15Ҏ্Ͱ͔͠࢖༻Ͱ͖ͳ͍͕ɺiOS 13 Ҏ্Ͱ࢖༻Ͱ͖ΔΑ͏ʹ͢Δܭը͕͋Δ(backport) • swiftcʹ͸Ϛʔδ͞Ε͍ͯΔ͕ɺXcode 13.0ͷஈ֊Ͱ͸࢖༻ Ͱ͖ͳ͍ • ΞϓϦʹConcurrencyʹඞཁͳϥϯλΠϜΛ૊ΈࠐΜͰ͠· ͍ɺiOS 15ະຬͰ΋࣮ߦͰ͖ΔΑ͏ʹ͍ͯ͠ΔΒ͍͠ Add an option to build the concurrency library for back 47

Slide 48

Slide 48 text

Xcode 13 Release Notes | Apple Developer Documentation 48

Slide 49

Slide 49 text

طଘͷΞϓϦͷ࣮ફతͳasync/awaitԽ ͜ͷηογϣϯ͕ྑ͔ͬͨ Swift concurrency: Update a sample app - WWDC21 - Videos - Apple Developer 49

Slide 50

Slide 50 text

ߟ͑Δ͜ͱ • εϨουຖʹactorΛ෼཭ • ϝΠϯεϨουͰͷ࣮ߦΛڧ੍͍ͨ͠ॲཧ(UI) • backgroundʹڲਖ਼͍ͨ͠ॲཧʢI/O΍ॏ͍ॲཧʣ • Ͳ͜Ͱ΋Α͍ॲཧ • asyncΠϯλʔϑΣΠεରԠ 50

Slide 51

Slide 51 text

51

Slide 52

Slide 52 text

52

Slide 53

Slide 53 text

53

Slide 54

Slide 54 text

• UI΍View͸MainActorʹ • ֎෦ͱͷI/O(͜͜Ͱ͸HealthKit)͸actorʹͯ͠ඇಉظͰѻ͏ • ͲͷεϨουͰॲཧ͢΂͖͔Λ೦಄ʹ૊Έସ͑Δͷ͕େࣄ 54

Slide 55

Slide 55 text

طଘͷ࣮૷ͷasyncԽ • Α͋͘Δૉ๿ͳඇಉظϝιου(completion) struct ImageDownloader { func downloadImage(url: URL, completion: @escaping (Result) -> Void) { var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if let error = error { completion(.failure(error)) } else { let image = data.flatMap(UIImage.init(data:)) ?? UIImage() completion(.success(image)) } } task.resume() } } 55

Slide 56

Slide 56 text

• withCheckedContinuation asyncͰ͸ͳ͍࣮૷Λϥοϓͯ͠؆୯ ʹasyncΠϯλʔϑΣΠεʹม׵Ͱ͖Δ public func withCheckedContinuation(_ body: (CheckedContinuation) -> Void) async -> T public func withCheckedThrowingContinuation(_ body: (CheckedContinuation) -> Void) async throws -> T 56

Slide 57

Slide 57 text

@available(iOS 15.0, *) extension ImageDownloader { func downloadImage(url: URL) async throws -> UIImage { try await withCheckedThrowingContinuation { continuation in downloadImage(url: url) { result in continuation.resume(with: result) } } } } 57

Slide 58

Slide 58 text

ࣗಈੜ੒ͯ͘͠ΕΔ • Editor > Refactor > Add Async Wrapper • completion൛ͷϝιουʹasync interfaceΛੜ΍͢ • Editor > Refactor > Add Async Alternative • async൛ͷϝιουʹલํޓ׵༻ͷcompletion handlerΛੜ΍ ͢ 58

Slide 59

Slide 59 text

59

Slide 60

Slide 60 text

Delegateύλʔϯͷasync/awaitԽ • جຊతʹ͸ಉ͡ͰCheckedContinuationΛอ࣋͢Δͱྑͦ͞͏ ྫɿURLSessionDataDelegateͷasync/awaitԽ 60

Slide 61

Slide 61 text

struct HTTPRedirectResolver { func resolve(_ url: URL) async throws -> URL? { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let delegate = Delegate(activeContinuation: continuation) let urlSession = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) let request = URLRequest(url: url) urlSession.dataTask(with: request).resume() } } private class Delegate: NSObject, URLSessionDataDelegate { private let activeContinuation: CheckedContinuation init(activeContinuation: CheckedContinuation) { self.activeContinuation = activeContinuation } func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { let newURL = request.url activeContinuation.resume(returning: newURL) completionHandler(nil) } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { activeContinuation.resume(returning: dataTask.currentRequest?.url) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard let error = error else { return activeContinuation.resume(returning: nil) } activeContinuation.resume(throwing: error) } } } 61

Slide 62

Slide 62 text

let resolver = URLRedirectResolver() let url = "http://moved-url.com" // redirect͢Δ let redirectedURL = try await resolver.resolve(url) 62

Slide 63

Slide 63 text

RxͰͷ࢖༻ • async/awaitΛObservableʹม׵ͯ͠ڠௐͯ͠࢖͑ΔΑ͏ʹ͢Δ 63

Slide 64

Slide 64 text

extension ObservableType { public static func create(throwingAsync callee: @escaping () async throws -> Element) -> Observable { Observable.create { observer in let task = Task { do { let result = try await callee() observer.onNext(result) } catch { observer.onError(error) } } return Disposables.create { task.cancel() } } } } 64

Slide 65

Slide 65 text

Observable.create { try async doSomething() } // Observable .asDriver(onErrorJustReturn: []) .drive(onNext: { _ in }) .dispose(by: disposeBag) • TaskΛ࢖͏͜ͱͰasyncΛӅṭ͍ͯ͠Δɻಉظͷੈք͔Β࢖͑Δ 65

Slide 66

Slide 66 text

ײ૝ɺٞ࿦ • ͍͟࢖͑ΔΑ͏ʹͳͬͨΒ෦෼తʹऔΓೖΕ͍ͯ͘ͷ͸༰қͦ ͏ • VS Rx • ௨৴ͷ଴ͪड͚ͳͲʹRxΛར༻͍ͯ͠Δ৔߹ͳͲ͸async/ awaitʹஔ͖׵͑ͯγϯϓϧʹॻ͚ͦ͏͕ͩɺ׬શʹஔ͖׵͑ ΒΕΔΘ͚Ͱ͸ແͦ͞͏ 66

Slide 67

Slide 67 text

• Actor • ࠓҎ্ʹUI૚ͱͦΕҎ֎ͷ૚ͷ֊૚Խ͕ॏཁʹࢥ͑Δ • View૚͸શͯ MainActor ʹͯ͠͠·͍ɺnonisolatedͳॲཧ ͸࣋ͨͤͳ͍ੈք͕ΩϨΠͦ͏ 67

Slide 68

Slide 68 text

• ࠓ͸back portͷ໰୊͕͋ͬͯɺطଘͷΞϓϦͰࠓ͙͢࢖͏ͷ͸ ೉͍͠ • Xcode 13.1ͱ͔ૣ͍ஈ֊Ͱಈ͘Α͏ʹͳΓͦ͏ͳ༧ײ • Development TargetΛiOS 15ʹͯ͠༡Ϳ͜ͱ͸Ͱ͖ͨ 68

Slide 69

Slide 69 text

͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ 69