【Swift】URLSession.shared.dataTask() をうまく使いこなせない

これは、犬の写真を取得する既存のコードです。

URLSessionで completionHandlerベースの便利なメソッドを使用しています。

コードは簡単なように見えて、私のテストでうまくいきましたが、少なくとも3つの間違いがあります。流れ順に見てみましょう。

dataTask を作成して resume します。そして、タスクが完了したら、completionHandler で応答を確認し、画像を作成し終了します。前後しながら流れていきます。

スレッドはどうでしょう。

小さなコードなのに驚くほど複雑で合計で3つの異なる実行コンテキストがあります。

最も外側のレイヤーは呼び出し元のスレッドまたはキューで実行され、completionHandler はセッションのデリゲートキューで実行され、最後に completionHandler はメインキューで実行されます。コンパイラでは捕捉できないので、スレッドの問題を避けるために細心の注意を払う必要があります。

今気づきましたが、completionHandler の呼び出しは、メインキューに一貫してディスパッチされません。これはバグかもしれません。

また、早期リターンをしていないのでエラーが発生した場合、completionHandler を2回呼び出すことになります。これは、作成者の意図と違う可能性があります。

また、最後の UIImage の作成は失敗する可能性があります。データが誤った形式の場合、この UIImage は nil を返すので、nil 画像と nil エラーの両方で completionHandler を呼び出すでしょう。

 

💡 新しい API


URLSession.shared.data()
URLSession.shared.upload()
URLSession.shared.download()
URLSession.shared.bytes()

👉 URLSession | Apple Developer Documentation hatena-bookmark

以下、参考にしたい新しいAPIのサンプルコードです。


// Fetch photo with async/await

func fetchPhoto(url: URL) async throws -> UIImage {
  let (data, response) = try await URLSession.shared.data(from: url)

  guard let httpResponse = response as? HTTPURLResponse,
        httpResponse.statusCode == 200 else {
    throw WoofError.invalidServerResponse
  }

  guard let image = UIImage(data: data) else {
    throw WoofError.unsupportedImage
  }
  return image
}


// URLSession.data

let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 
else {
  throw MyNetworkingError.invalidServerResponse
}


// URLSession.upload

var request = URLRequest(url: url)
request.httpMethod = "POST"

let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 201
else {
  throw MyNetworkingError.invalidServerResponse
}


// URLSession.download

let (location, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 
else {
  throw MyNetworkingError.invalidServerResponse
}

try FileManager.default.moveItem(at: location, to: newLocation)


// Cancellation

let task = Task {
  let (data1, response1) = try await URLSession.shared.data(from: url1)
  let (data2, response2) = try await URLSession.shared.data(from: url2)
}

task.cancel()


// asyncSequence demo

let (bytes, response) = try await URLSession.shared.bytes(from: Self.eventStreamURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
  throw WoofError.invalidServerResponse
}

for try await line in bytes.lines {
  let photoMetadata = try JSONDecoder().decode(PhotoMetadata.self, from: Data(line.utf8))
  await updateFavoriteCount(with: photoMetadata)
}


// task specific delegate demo

class AuthenticationDelegate: NSObject, URLSessionTaskDelegate {
  private let signInController: SignInController
    
  init(signInController: SignInController) {
    self.signInController = signInController
  }
    
  func urlSession(_ session: URLSession,
                  task: URLSessionTask,
                  didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
      do {
        let (username, password) = try await signInController.promptForCredential()
          return (.useCredential, URLCredential(user: username, password: password, persistence: .forSession))
      } catch {
        return (.cancelAuthenticationChallenge, nil)
      }
    } else {
      return (.performDefaultHandling, nil)
    }
  }
}

最初の async/await のコードをベースに、どれかのパターンで対応できそうです。

確かに使いやすそうです。

 

💡 まとめ

最近のプログラミング言語は仕様の変化が速いので、ネットで検索するとどれを使ったらいいのか、私たち初心者は混乱します。

ありがたい Apple 公式の公開資料からでした。

👉 Use async/await with URLSession - WWDC21 - Videos - Apple Developer hatena-bookmark

次は、人気の

Alamofire/Alamofire: Elegant HTTP Networking in Swift hatena-bookmark

を使ってみたいと思っています。


// Automatic String to URL conversion, Swift concurrency support, and automatic retry.
let response = await AF.request("https://httpbin.org/get", interceptor: .retryPolicy)
                       // Automatic HTTP Basic Auth.
                       .authenticate(username: "user", password: "pass")
                       // Caching customization.
                       .cacheResponse(using: .cache)
                       // Redirect customization.
                       .redirect(using: .follow)
                       // Validate response code and Content-Type.
                       .validate()
                       // Produce a cURL command for the request.
                       .cURLDescription { description in
                         print(description)
                       }
                       // Automatic Decodable support with background parsing.
                       .serializingDecodable(DecodableType.self)
                       // Await the full response with metrics and a parsed body.
                       .response
// Detailed response description for easy debugging.
debugPrint(response)

どうぞよろしくおねがいします。


SwiftUI + SwiftData で ToDo リスト を作ってみる

SwiftUI、SwiftData の公式チュートリアルってすごく良いですね

参考にしながら、まずは、Todo リスト的なものを作ってみました。

Landmarks というサンプルコードのタブに追加してみました。

Android の「Jetpack Compose + ストレージ的な何か」で作るより、簡単に直感的に作れるような気がしています。

もっと上手にプログラミングできるよう日々精進していこうと思います。


【Xcode】Preview Crashed で 表示されない 📲

Xcode の Preview がクラッシュしっぱなし。

【Xcode】Preview Crashed で 表示されない

ストレージまわりの話ではないかと思いながらしらべる。


xcrun simctl -—set previews delete all

👉 Preview keeps crashing | Apple Developer Forums hatena-bookmark
👉 SwiftUIのプレビューが動かなくなった時の対処方法 #SwiftUI - Qiita hatena-bookmark

 

📲 ログから原因を見つける

まずは、見てみますか。


#!/bin/bash

cd ~/Library/Logs/DiagnosticReports
file=$(ls XCPreview* | head -1)
open $file

何か起こったらログ確認大事。

 

📲 対応方法

いろいろありそうです。

このシミュレータですが、通常のシミュレータとは別でXcodeが持っています。しかしストレージ管理のデベロッパ欄には表示されない上に自動で削除されません。私の環境ではiOS14.1のシミュレータなども残っていました。

👉 【SwiftUI】PreviewのSimulator残留 | thwork hatena-bookmark


cd ~/Library/Developer/Xcode/UserData/Previews/Simulator\ Devices/
find . -name com.apple.suggestions.plist -exec plutil -replace SuggestionsAppLibraryEnabled -bool NO {} ";"

👉 [Tips]Xcode13.1のSwiftUIのpreviewでCPU使用率が高くなるのをSpotlightを止めて回避する(iOS15) #iOS - Qiita hatena-bookmark

- Restart the computer and re-run xcrun simctl --set previews delete all
- Delete stored Preview data directly. This data is stored in ~/Library/Developer/Xcode/UserData/Previews.

👉 Use Realm with SwiftUI Previews — Realm hatena-bookmark


xcrun simctl --set previews delete unavailable | all

👉 SwiftUI Previews - Disk space issue | Apple Developer Forums hatena-bookmark

Save space by
1. xcrun simctl delete unavailable
2. xcrun simctl erase all
3. Delete /Users/username/Library/Developer/CoreSimulator folder.

👉 swiftui - Xcode 13 UI Previews folder takes too much space - Stack Overflow hatena-bookmark

 

📲 xcrun simctl とは ?

コマンドラインツールから help を確認する。


❯ xcrun simctl help
usage: simctl [--set <path>] [--profiles <path>] <subcommand> ...
       simctl help [subcommand]
Command line utility to control the Simulator

For subcommands that require a <device> argument, you may specify a device UDID
or the special "booted" string which will cause simctl to pick a booted device.
If multiple devices are booted when the "booted" device is selected, simctl
will choose one of them.

Subcommands:
	addmedia            Add photos, live photos, videos, or contacts to the library of a device.
	boot                Boot a device or device pair.
	clone               Clone an existing device.
	create              Create a new device.
	delete              Delete specified devices, unavailable devices, or all devices.
	diagnose            Collect diagnostic information and logs.
	erase               Erase a device's contents and settings.
	get_app_container   Print the path of the installed app's container
	getenv              Print an environment variable from a running device.
	help                Prints the usage for a given subcommand.
	icloud_sync         Trigger iCloud sync on a device.
	install             Install an app on a device.
	install_app_data    Install an xcappdata package to a device, replacing the current contents of the container.
	io                  Set up a device IO operation.
	keychain            Manipulate a device's keychain
	launch              Launch an application by identifier on a device.
	list                List available devices, device types, runtimes, or device pairs.
	location            Control a device's simulated location
	logverbose          enable or disable verbose logging for a device
	openurl             Open a URL in a device.
	pair                Create a new watch and phone pair.
	pair_activate       Set a given pair as active.
	pbcopy              Copy standard input onto the device pasteboard.
	pbpaste             Print the contents of the device's pasteboard to standard output.
	pbsync              Sync the pasteboard content from one pasteboard to another.
	privacy             Grant, revoke, or reset privacy and permissions
	push                Send a simulated push notification
	rename              Rename a device.
	runtime             Perform operations on runtimes
	shutdown            Shutdown a device.
	spawn               Spawn a process by executing a given executable on a device.
	status_bar          Set or clear status bar overrides
	terminate           Terminate an application by identifier on a device.
	ui                  Get or Set UI options
	uninstall           Uninstall an app from a device.
	unpair              Unpair a watch and phone pair.
	upgrade             Upgrade a device to a newer runtime.

❯ xcrun simctl help delete
Delete specified devices, unavailable devices, or all devices.
Usage: simctl delete <device> [... <device n>] | unavailable | all

Specifying unavailable will delete devices that are not supported by the current Xcode SDK.

❯ xcrun simctl list devices
== Devices ==
-- iOS 17.0 --
    iPhone SE (3rd generation) (EE4D7500-5439-4D41-AD42-6028F649FA44) (Shutdown)
    iPhone 15 (984F204F-2D4E-4E56-8EEE-D11C46EDD9C2) (Shutdown)
    iPhone 15 Plus (C1349C55-57AC-4E47-9EF3-71A848E24A4E) (Shutdown)
    iPhone 15 Pro (04DC04FF-87DB-4EB6-91D8-C8CB4CF10015) (Shutdown)
    iPhone 15 Pro Max (5DC11544-4C3E-4852-8899-3D9E0844519B) (Shutdown)
    iPad Air (5th generation) (906B44B3-B298-4CF1-A5FB-0B4D43277636) (Shutdown)
    iPad (10th generation) (3804C1FA-6306-4CB7-98E0-D24CAFA70537) (Shutdown)
    iPad mini (6th generation) (351ED4C6-2DF3-4F1F-A29A-6D44483D9A95) (Shutdown)
    iPad Pro (11-inch) (4th generation) (96D48442-96AA-421A-BBA6-6F45EE8ECDBE) (Shutdown)
    iPad Pro (12.9-inch) (6th generation) (0B527419-DE2A-4146-93C4-55540B624C5C) (Shutdown)

❯ xcrun simctl --set previews list devices
Using Previews Device Set: '/Users/mao/Library/Developer/Xcode/UserData/Previews/Simulator Devices'
== Devices ==
-- iOS 17.0 --
    iPhone 15 Pro (676858D4-35E8-4841-91F4-17804B3E7565) (Shutdown)

覚えておくべきは、

Preview は、シュミレータ単独と別枠でシュミレータを使っている。

ということ。

 

📲 まとめ

Preview と シュミレーターに関連する問題は多そうです。

  • CPU 使用率
  • ストレージ占有
  • キャッシュ・データ不整合

私の場合スッキリ対応した方法としては、以下を Xcode を一旦終了して実行。


xcrun simctl --set previews delete all
rm -rf ~/Library/Developer/Xcode/UserData/Previews
rm -rf ~/Library/Developer/CoreSimulator/Caches

👉 How to clear/reset SwiftUI Preview Caches? hatena-bookmark

Behavors に入れときますか、消した previews に関しては自動で作成されると思って良さそう。