Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
今日から使える実践的Swift Concurrency / Introducing Swift...
Search
giginet
PRO
September 28, 2021
Technology
410
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
今日から使える実践的Swift Concurrency / Introducing Swift Concurrency
giginet
PRO
September 28, 2021
More Decks by giginet
See All by giginet
🀄️ on swiftc
giginet
PRO
0
610
15年メンテしてきたdotfilesから開発トレンドを振り返る 2011 - 2026
giginet
PRO
2
510
Claude Codeログ基盤の構築
giginet
PRO
7
4.7k
Inside of Swift Export
giginet
PRO
1
3.6k
Swiftビルド弾丸ツアー - Swift Buildが作る新しいエコシステム
giginet
PRO
0
3k
SwiftでMCPサーバーを作ろう!
giginet
PRO
3
500
Creating Intuitive Developer Tool in Swift
giginet
PRO
0
1.7k
Mergeable Libraryで 高速なアプリ起動を実現しよう!
giginet
PRO
2
8.7k
5分でわかるExplicitly Built Modules
giginet
PRO
2
1.9k
Other Decks in Technology
See All in Technology
Autonomous AI Lakehouse - Live AI Hubから始めるセキュアなAIデータ活用 -
oracle4engineer
PRO
2
150
ドメインエキスパートは いかにして AI を使って プロダクト開発をするか
tatsuyashiraishi
1
190
FinOps Agentが生んだ技術負債:コスト自動調査の落とし穴と解消までの道のり
oasis1994liveforever
3
300
背中から、背中へ ー 社外コミュニティで学んだ「雑に背中を押す」という恩送り ー / easygoing encouragement and pay forward to community
naitosatoshi
0
320
Gemini Intelligence 入門 ― AppFunctions と Gemini Nano
takuroabe
0
380
ビジネス部門からPOを迎えての半年間の進歩と課題
zozotech
PRO
0
150
AI時代のGraphQL
joere
3
410
局所最適は悪ではない。壊れているのは指標です。量と質を同時に見る先行指標の設計
recruitengineers
PRO
2
1.2k
顧客価値を追いかけたら、プライシングにたどり着いた
sansantech
PRO
2
1k
AI時代、つくる力を自分たちの手に。 ──電通×KAG、アジャイルとAI駆動開発で挑む開発の変革
yuukiyo
3
390
freeeにおけるSoftware Factory
freee
PRO
0
520
AIエージェント時代、Kaggleにどう取り組むか
takanao
1
180
Featured
See All Featured
Ethics towards AI in product and experience design
skipperchong
2
440
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.9k
Are puppies a ranking factor?
jonoalderson
2
4.1k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
720
How to Think Like a Performance Engineer
csswizardry
28
2.9k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
74
43k
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
790
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
610
Tips & Tricks on How to Get Your First Job In Tech
honzajavorek
1
810
Tell your own story through comics
letsgokoyo
1
1.1k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
2.5k
Transcript
ࠓ͔Β͑Δ࣮ફతSwift Concurrency @giginet 1
ࣗݾհ • @giginet • Core Contributor of Carthage/fastlane/ XcodeGen etc...
• https://github.com/giginet • https://twitter.com/giginet • ΫοΫύου(2015/4~) ϞόΠϧج൫ ෦ • ؾܰʹmention͍ͯͩ͘͠͞ʂ 2
iOSDC ͠·ͨ͠ େنͳΞϓϦͷϚϧνϞδϡʔϧߏͷ࣮ફ by giginet | τʔΫ | iOSDC Japan
2021 #iosdc - fortee.jp 3
ٕज़ސgiginet͞ΜΠϯλϏϡʔ ίϛϡχςΟ׆ಈOSS׆ಈͷ ൿ݃Λฉ͍ͯΈ·ͨ͠ | Money Forward Engineers' Blog 4
Agenda • Swift Concurrency·ͱΊ • جຊฤ • Structured Concurrency •
actor 5
• ࣮ફతSwift Concurrency • طଘͷΞϓϦͷҠߦ • Α͋͘Δ࣮ΛConcurrencyԽͯ͠ΈΑ͏ • callbackύλʔϯ •
Delegateύλʔϯ • RxSwiftͱͷڠௐ • σΟεΧογϣϯ 6
Swift Concurrency·ͱΊ ͍Ζ͍Ζࢿྉ͕͋ΔͷͰجຊͦͪΒΛݟΕྑ͍ɻࠓճ͠ ͍ํ͚ʹ͜ΕΛϕʔεʹ؆୯ʹઆ໌͍͖ͯ͠·͢ɻ • WWDC 2021ͷSwiftͷฒߦॲཧؔ࿈ͷηογϣϯҰཡ - Qiita •
async/awaitactorͰiOSΞϓϦ։ൃ͕Ͳ͏มΘΔ͔ Before & Afterͷ۩ମྫͰֶͿ - Speaker Deck • Swift Concurrency νʔτγʔτ 7
Swift Concurrencyجຊฤ 8
• Α͋͘ΔͭɻcallbackΛड͚औͬͯResultͰΒ͏ Before func downloadData(from url: URL, completion:@escaping (Result<Data, Error>)
-> Void) downloadData(from: url) { result in switch result { case .success(let data): // data Λ͏ॲཧ case .failure(let error): // ΤϥʔϋϯυϦϯά } } 9
After func downloadData(from url: URL) async throws -> Data do
{ let data = try await downloadData(from: url) // data Λ͏ॲཧ } catch { // ΤϥʔϋϯυϦϯά } 10
ྫ2 • ·ͣϢʔβʔσʔλͷJSONΛΒͬͯɺͦͷJSONΛσίʔ υɺͦͷதʹ͋ΔURL͔Βը૾Λऔಘ͢Δ 11
func fetchUserIcon(for id: User.ID, completion: @escaping (Result<Data, Error>) -> 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
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
ྫ3 • JSONͷதʹ2ͭͷը૾URLؚ͕·Ε͍ͯΔ(smallIconImage, largeIconImage) • ·ͣJSONΛऔಘ͠ɺؚ·ΕΔը૾Λฒߦ(Parallel)ͯ͠μϯ ϩʔυ͍ͨ͠ 14
• JSONͷऔಘޙʹը૾ͷऔಘʢґଘؔˠίʔϧόοΫͷ࿈ʣ • 2ͭͷը૾Parallelʹऔಘ͢Δඞཁ͕͋Δ • GCDͷDispatchGroupͳͲΛར༻ 15
! 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<Data, Error>! group.enter() downloadData(from: user.smallURL) { icon in smallIcon = icon group.leave() } var largeIcon: Result<Data, Error>! 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
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
Structured Concurrency • ෳͷasyncॲཧΛߏԽ࣮ͯ͠ߦ͢ΔΈ • Task • ্هͷྫͷΑ͏ʹෳͷλεΫΛฒߦͯ͠ߦͬͨΓ • ࢠλεΫΛ࡞ͬͨΓ
• Ωϟϯηϧͨ͠Γ • ͕ՄೳʹͳΔ 18
• swift-evolution/0304-structured-concurrency.md at main · apple/swift-evolution • Explore structured concurrency
in Swift - WWDC21 - Videos - Apple Developer 19
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
21
Task • ඇಉظؔTaskͰϥοϓ͢Δ͜ͱͰಉظͷੈք͔ΒͰ࣮ߦ Ͱ͖Δ • ಉظੈքͱඇಉظੈքͷڮ͠ʹ͑Δ func syncMethod() { Task
{ await doSomething() } } 22
Cancel func syncMethod() { let task = Task { await
doSomething() } task.cancel() } • TaskcancelͰ͖Δ • ϘλϯΛԡͨ͠ΒTaskΛൃՐ͢Δ͕ɺऴΘΔલʹΩϟϯηϧϘ λϯΛԡͨ͠ΒऴྃΈ͍ͨͳͷ͕ग़དྷΔ • ࢠλεΫ·ͱΊͯΩϟϯηϧ͞ΕΔ 23
AsyncSequence • asyncʹ͕औΕΔiterator • ϑΝΠϧI/Oͱ͔ • streamͱ͔(WebSocketͱ͔) • ࢠλεΫΛಈతʹ૿ͯ͠ɺiteratorͷΑ͏ʹѻ͑Δ Meet
AsyncSequence - WWDC21 - Videos - Apple Developer 24
actor • ඇಉظͷσʔλڝ߹Λղܾ͢Δ͘͠Έ • ॲཧΛඇಉظʹͨ͠ΓɺΩϡʔΛ੍ͨ͠ΓɺϩοΫͷऔಘΛ ࣗಈతʹͬͯ͘ΕΔ 25
σʔλڝ߹ͱ • ҎԼͷ߹ɺյΕͯ͠·͏ let counter: Counter = .init() DispatchQueue.global().async {
print(counter.increment()) // ? } DispatchQueue.global().async { print(counter.increment()) // ? } 26
actor Counter { private var count: Int = 0 func
increment() -> Int { count += 1 return count } } 27
• actorͷશͯͷϓϩύςΟϝιου͕ඇಉظʹͳΔͨΊɺ σʔλڝ߹͔ΒकΒΕΔ • actor෦ʹઐ༻ͷΩϡʔ(serial executor)Λอ͓࣋ͯ͠ΓɺӅ ṭ͞ΕΔ await counter.increment() 28
• 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
Global Actor • actorͷ༻ΩϡʔΛಛఆͷΩϡʔʹ੍ݶ͢ΔͨΊͷ͘͠Έ • SEͰҙΩϡʔ͕ఏҊ͞Ε͍ͯΔ͕ɺݱࡏMainActorͷ Έ 30
@MainActor final class MyViewController: UIViewController { func doSomething() { }
} • actorͰͳ͘ɺطଘͷΫϥεʹ@MainActorΛ͚Δͱϝϯό͕ શ෦asyncʹͳΓϝΠϯεϨουͰ࣮ߦ͞ΕΔ 31
• ֎͔ΒϝιουϓϩύςΟ͕asyncʹݟ͑Δ • MainActorʹ͍Δͷͷத͔Β͔͋ͨಉظతʹݺΔ • MainActorҎ֎͔Βͷ࣮ߦΛ͋Δఔ͙͜ͱ͕ग़དྷΔ • UIͷߋ৽ͳͲɺϝΠϯεϨουҎ֎Ͱ࣮ߦ͞Εͯཉ͘͠ͳ͍ ͷΛؒҧͬͯݺͳ͍Α͏ʹίϯύΠϧ࣌ʹอޢ͢Δ͜ͱ ͕Ͱ͖Δ
32
@MainActor func reloadData() { // ϝΠϯεϨουͰ࣮ߦͯ͠ཉ͍͠ॲཧ } @MainActor final class
SomeViewController: UIViewController { override func viewDidLoad() { reloadData() } } func callReloadData() { let vc = SomeViewController() vc.reloadData() // ϝΠϯεϨουอূ͕ͳ͍ͷͰasync͡Όͳ͍ͱݺͳ͍ } 33
UIViewController • υΩϡϝϯτΛݟΔײ͡ɺUIViewControllerશͯ@MainActor ʹͳ͍ͬͯΔ • https://developer.apple.com/documentation/uikit/ uiviewcontroller?changes=latest_minor • ࣮ࡍʹ͜ΕΛΔͱଟ͘ͷϓϩμΫτͰޓੑ͕ҡ࣋Ͱ͖ͳ͘ ͳΔΑͳͱࢥ͍ͬͯΔɻͳΜ͔Έͨײ͡దԠ͞Εͯແͦ͞͏
ͰΑ͘Θ͔Βͳ͍ ! 34
35
class User { var name: String var age: Int }
actor Session { var currentUser: User? } 36
func run() async { let session = Session() let user
= await session.currentUser user?.age += 1 } 37
38
ͳͥʁ • UserࢀরܕͰࢀরઌͰ͕มΘΓ͏Δ • ෳͷλεΫͰࢀর͍ͯ͠Δؒʹ1ܦͬͯage͕૿͑ΔՄೳੑ ͕͋Δ • σʔλڝ߹ͷةݥੑ͕͋Δ 39
Sendable • ͜ͷܕ͕actorؒͰΓͱΓͰ͖Δ͜ͱΛprotocolʹ௨͢Δ protocol(marker protocol) • swift-evolution/0302-concurrent-value-and-concurrent- closures.md at main
· apple/swift-evolution 40
class User: Sendable { let user: String let age: Int
} • Sendableʹద߹͢Δ͜ͱͰίϯύΠϥʹ҆શʹड͚͠Ͱ͖Δ ܕͰ͋Δ͜ͱΛ௨ • ্هͷΑ͏ͳΠϛϡʔλϒϧΫϥε(varΛ࣋ͨͣʹҰ࡞ͬͨ ΒมΘΒͳ͍)SendableʹͰ͖Δ 41
• ͜ͷܯࠂXcode 13.0(Swift 5.5)ͷ࣌Ͱແޮʹͳ͍ͬͯΔ (؇͔ͳҠߦͷͨΊ) • Swift 6.0͔Βܯࠂ͕ඪ४Ͱग़ΔΑ͏ʹͳΔͷͰিܸʹඋ͑Δ • -warn-concurencyΦϓγϣϯͰܯࠂΛ༗ޮԽͰ͖Δ
42
• Swift ConcurrencyͷwithTaskCancellationHandlerͱSendable - cockscomblog? • Concurrency in Swift 5
and 6 - Evolution / Discussion - Swift Forums 43
Concurrencyͷར • ॲཧͷߏԽ • ॲཧͷྲྀΕΛಡΈԼ͍͍ͤͯ͢ • ݕࠪྫ֎ͷੵۃతͳ༻ • σʔλڝ߹ͷίϯύΠϧ࣌νΣοΫ •
࣮ߦεϨουͷίϯύΠϧ࣌νΣοΫ 44
Special Thanks • ͜͜·ͰͷεϥΠυͷ΄ͱΜͲ @koher ͞Μͷࢿྉͷ௨ΓͰ ͢ ! • ૉΒ͍͠ͷͰαϙʔτ͠·ͨ͠ɻ͋Γ͕ͱ͏͍͟͝·͢
45
໌͔Β͑ΔSwift Concurrency • طଘͷΞϓϦΛ෦తʹSwift ConcurrencyରԠ͍ͯ͘͠ςΫ χοΫ 46
Swift Concurrency backport • ݱࡏɺConcurrencyiOS 15Ҏ্Ͱ͔͠༻Ͱ͖ͳ͍͕ɺiOS 13 Ҏ্Ͱ༻Ͱ͖ΔΑ͏ʹ͢Δܭը͕͋Δ(backport) • swiftcʹϚʔδ͞Ε͍ͯΔ͕ɺXcode
13.0ͷஈ֊Ͱ༻ Ͱ͖ͳ͍ • ΞϓϦʹConcurrencyʹඞཁͳϥϯλΠϜΛΈࠐΜͰ͠· ͍ɺiOS 15ະຬͰ࣮ߦͰ͖ΔΑ͏ʹ͍ͯ͠ΔΒ͍͠ Add an option to build the concurrency library for back 47
Xcode 13 Release Notes | Apple Developer Documentation 48
طଘͷΞϓϦͷ࣮ફతͳasync/awaitԽ ͜ͷηογϣϯ͕ྑ͔ͬͨ Swift concurrency: Update a sample app - WWDC21
- Videos - Apple Developer 49
ߟ͑Δ͜ͱ • εϨουຖʹactorΛ • ϝΠϯεϨουͰͷ࣮ߦΛڧ੍͍ͨ͠ॲཧ(UI) • backgroundʹڲਖ਼͍ͨ͠ॲཧʢI/Oॏ͍ॲཧʣ • Ͳ͜ͰΑ͍ॲཧ •
asyncΠϯλʔϑΣΠεରԠ 50
51
52
53
• UIViewMainActorʹ • ֎෦ͱͷI/O(͜͜ͰHealthKit)actorʹͯ͠ඇಉظͰѻ͏ • ͲͷεϨουͰॲཧ͖͔͢Λ೦಄ʹΈସ͑Δͷ͕େࣄ 54
طଘͷ࣮ͷasyncԽ • Α͋͘Δૉͳඇಉظϝιου(completion) struct ImageDownloader { func downloadImage(url: URL, completion:
@escaping (Result<UIImage, Error>) -> 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
• withCheckedContinuation asyncͰͳ͍࣮Λϥοϓͯ͠؆୯ ʹasyncΠϯλʔϑΣΠεʹมͰ͖Δ public func withCheckedContinuation<T>(_ body: (CheckedContinuation<T, Never>)
-> Void) async -> T public func withCheckedThrowingContinuation<T>(_ body: (CheckedContinuation<T, Error>) -> Void) async throws -> T 56
@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
ࣗಈੜͯ͘͠ΕΔ • Editor > Refactor > Add Async Wrapper •
completion൛ͷϝιουʹasync interfaceΛੜ͢ • Editor > Refactor > Add Async Alternative • async൛ͷϝιουʹલํޓ༻ͷcompletion handlerΛੜ ͢ 58
59
Delegateύλʔϯͷasync/awaitԽ • جຊతʹಉ͡ͰCheckedContinuationΛอ࣋͢Δͱྑͦ͞͏ ྫɿURLSessionDataDelegateͷasync/awaitԽ 60
struct HTTPRedirectResolver { func resolve(_ url: URL) async throws ->
URL? { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<URL?, Error>) 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<URL?, Error> init(activeContinuation: CheckedContinuation<URL?, Error>) { 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
let resolver = URLRedirectResolver() let url = "http://moved-url.com" // redirect͢Δ
let redirectedURL = try await resolver.resolve(url) 62
RxͰͷ༻ • async/awaitΛObservableʹมͯ͠ڠௐͯ͑͠ΔΑ͏ʹ͢Δ 63
extension ObservableType { public static func create(throwingAsync callee: @escaping ()
async throws -> Element) -> Observable<Element> { Observable<Element>.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
Observable.create { try async doSomething() } // Observable<T> .asDriver(onErrorJustReturn: [])
.drive(onNext: { _ in }) .dispose(by: disposeBag) • TaskΛ͏͜ͱͰasyncΛӅṭ͍ͯ͠Δɻಉظͷੈք͔Β͑Δ 65
ײɺٞ • ͍͑͟ΔΑ͏ʹͳͬͨΒ෦తʹऔΓೖΕ͍ͯ͘ͷ༰қͦ ͏ • VS Rx • ௨৴ͷͪड͚ͳͲʹRxΛར༻͍ͯ͠Δ߹ͳͲasync/ awaitʹஔ͖͑ͯγϯϓϧʹॻ͚ͦ͏͕ͩɺશʹஔ͖͑
ΒΕΔΘ͚Ͱແͦ͞͏ 66
• Actor • ࠓҎ্ʹUIͱͦΕҎ֎ͷͷ֊Խ͕ॏཁʹࢥ͑Δ • Viewશͯ MainActor ʹͯ͠͠·͍ɺnonisolatedͳॲཧ ࣋ͨͤͳ͍ੈք͕ΩϨΠͦ͏ 67
• ࠓback portͷ͕͋ͬͯɺطଘͷΞϓϦͰࠓ͙͢͏ͷ ͍͠ • Xcode 13.1ͱ͔ૣ͍ஈ֊Ͱಈ͘Α͏ʹͳΓͦ͏ͳ༧ײ • Development TargetΛiOS
15ʹͯ͠༡Ϳ͜ͱͰ͖ͨ 68
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ 69