Upgrade to Pro — share decks privately, control downloads, hide ads and more …

今日から使える実践的Swift Concurrency / Introducing Swift Concurrency

giginet
PRO
September 28, 2021

今日から使える実践的Swift Concurrency / Introducing Swift Concurrency

giginet
PRO

September 28, 2021
Tweet

More Decks by giginet

Other Decks in Technology

Transcript

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

    View Slide

  2. ࣗݾ঺հ
    • @giginet
    • Core Contributor of Carthage/fastlane/
    XcodeGen etc...
    • https://github.com/giginet
    • https://twitter.com/giginet
    • ΫοΫύου(2015/4~) ϞόΠϧج൫

    • ؾܰʹmention͍ͯͩ͘͠͞ʂ
    2

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. Swift Concurrencyجຊฤ
    8

    View Slide

  9. • Α͋͘Δ΍ͭɻ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

    View Slide

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

    View Slide

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

    View Slide

  12. 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

    View Slide

  13. 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

    View Slide

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

    View Slide

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

    View Slide

  16. !
    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

    View Slide

  17. 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

    View Slide

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

    View Slide

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

    View Slide

  20. 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

    View Slide

  21. 21

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  29. • 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. 35

    View Slide

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

    View Slide

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

    View Slide

  38. 38

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. 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

    View Slide

  48. Xcode 13 Release Notes | Apple Developer Documentation
    48

    View Slide

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

    View Slide

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

    View Slide

  51. 51

    View Slide

  52. 52

    View Slide

  53. 53

    View Slide

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

    View Slide

  55. طଘͷ࣮૷ͷ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

    View Slide

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

    View Slide

  57. @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

    View Slide

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

    View Slide

  59. 59

    View Slide

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

    View Slide

  61. 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

    View Slide

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

    View Slide

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

    View Slide

  64. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide