20211127のSwiftに関する記事は5件です。

WidgetKit、複数のWidgetを持つ

ドキュメント「Creating a Widget Extension」 https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension ドキュメントの最後に「Declare Multiple Widgets in Your App Extension」 DeepLで翻訳 App Extensionで複数のウィジェットを宣言する 上記のGameStatusWidgetの例では、@main属性を使用して、ウィジェットエクステンションの単一のエントリーポイントを指定しています。複数のウィジェットをサポートするには、複数のウィジェットを body プロパティでまとめた > WidgetBundle に準拠した構造体を宣言します。このwidget-bundle構造体に@main属性を追加して、拡張機能が複数のウィジェットをサポートしていることをWidgetKitに伝えます。 例えば、ゲームアプリがキャラクターのヘルスを表示する2つ目のウィジェットと、リーダーボードを表示する3つ目のウィジェットを持っていた場合、図のようにそれらをグループ化します。 変更してみる 元のソース import WidgetKit import SwiftUI struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date()) } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date } struct ExampleWidgetEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .time) } } @main struct ExampleWidget: Widget { let kind: String = "ExampleWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in ExampleWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } struct ExampleWidget_Previews: PreviewProvider { static var previews: some View { ExampleWidgetEntryView(entry: SimpleEntry(date: Date())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } 変更後のソース、変更箇所のみ struct ExampleWidget: Widget { let kind: String = "ExampleWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in ExampleWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } @main struct ExampleWidgets: WidgetBundle { @WidgetBundleBuilder var body: some Widget { ExampleWidget() ExampleWidget() } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】タイトルのフォントと文字サイズと文字色を同時指定する方法

SwiftでNavigationBarのタイトルのフォントと文字サイズと文字色を同時指定する方法。 同時指定する方法を解説している記事がなかったのでメモとして残しておきます。 実装コード self.title = "タイトル" self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 40), .foregroundColor: UIColor.white ] 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

URLSessionを使ってAPIクライアントを実装する

概要 APIクライアント実装のメモです。 環境 Xcode 13.1 以下の内容はXcodeのPlayground環境を使っています。 リクエストするためのprotocolを実装する GET・POSTリクエストをenumで定義します。 APIのURL, パス, HTTPメソッド, レスポンスを表現するプロトコルを実装します。 ApiSessionPlayground enum HttpMethod: String { case `get` = "GET" case post = "POST" } protocol Requestable { associatedtype Response: Decodable var baseURL: URL { get } var path: String { get } var httpMethod: HttpMethod { get } } レスポンスをResult<T>で表現する APIレスポンスの結果は成功か失敗かで表現します。 enumを定義します。 ApiSessionPlayground enum Result<T> { case success(T) case failure(Error) } APIリクエストを実行するクラスを定義する Sessionというクラスを定義しました。 API通信が終わった後にコールバックを実行します。 コールバックの引数には先ほど定義したResult<T>が入る想定です。 ApiSessionPlayground final class Session { func send<T: Requestable>(_ request: T, completion: @escaping (Result<T.Response>) -> ()) { } } URLRequestを作成する APIのURL, パス, HTTPメソッドからURLRequestを作成します。 ApiSessionPlayground final class Session { func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) { let url = requestable.baseURL.appendingPathComponent(requestable.path) var urlRequest = URLRequest(url: url) urlRequest.httpMethod = requestable.httpMethod.rawValue } } URLSessionDataTaskを準備する 先程のURLRequestからタスクを作成し実行します。 ApiSessionPlayground final class Session { func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) { let url = requestable.baseURL.appendingPathComponent(requestable.path) var urlRequest = URLRequest(url: url) urlRequest.httpMethod = requestable.httpMethod.rawValue let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in } task.resume() } } API通信時に発生するエラーを定義する エラーをよりきめ細かく検知するためにカスタムエラーを定義します。 ApisessionPlayground enum SessionError: Error { case noResponse case unacceptableStatuCode(Int) case noData } APIから受け取ったデータをハンドリングする 以下のことを行なっています。 1. レスポンスを HTTPURLResponseに変換する 2. ステータスコードを確認する 3. データをデコードする ApiSessionPlayground final class Session { func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) { let url = requestable.baseURL.appendingPathComponent(requestable.path) var urlRequest = URLRequest(url: url) urlRequest.httpMethod = requestable.httpMethod.rawValue let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error = error { completion(.failure(error)) return } guard let response = response as? HTTPURLResponse else { completion(.failure(SessionError.noResponse)) return } guard 200..<300 ~= response.statusCode else { completion(.failure(SessionError.unacceptableStatuCode(response.statusCode))) return } guard let data = data else { completion(.failure(SessionError.noData)) return } do { let objects = try JSONDecoder().decode(T.Response.self, from: data) completion(.success(objects)) } catch { completion(.failure(error)) } } task.resume() } } QiitaのAPIを叩いてみる QiitaAPIを叩くための構造体を定義します。 また、Qiitaの投稿を表現する構造体を定義します。 ApiSessionPlayground struct Article: Codable { let title: String } struct ArticlesApiRequestable: Requestable { typealias Response = [Article] var baseURL: URL = URL(string: "https://qiita.com")! var path: String = "/apiv2/items" var httpMethod: HttpMethod = .get } APIリクエストを実行する。 getArticles()という関数を定義して、APIをコールします。 ApiSessionPlayground func getArticles() { let requestable = ArticlesApiRequestable() let session = Session() session.send(requestable) { result in switch result { case .success(let articles): print(articles.count) case .failure(let error): print(error) } } } getArticles() 全体のコードは以下のようになります。 ApiSessionPlayground import Foundation enum SessionError: Error { case noResponse case unacceptableStatuCode(Int) case noData } enum Result<T> { case success(T) case failure(Error) } enum HttpMethod: String { case `get` = "GET" case post = "POST" } protocol Requestable { associatedtype Response: Decodable var baseURL: URL { get } var path: String { get } var httpMethod: HttpMethod { get } } final class Session { func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) { let url = requestable.baseURL.appendingPathComponent(requestable.path) var urlRequest = URLRequest(url: url) urlRequest.httpMethod = requestable.httpMethod.rawValue let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in if let error = error { completion(.failure(error)) return } guard let response = response as? HTTPURLResponse else { completion(.failure(SessionError.noResponse)) return } guard 200..<300 ~= response.statusCode else { completion(.failure(SessionError.unacceptableStatuCode(response.statusCode))) return } guard let data = data else { completion(.failure(SessionError.noData)) return } do { let objects = try JSONDecoder().decode(T.Response.self, from: data) completion(.success(objects)) } catch { completion(.failure(error)) } } task.resume() } } struct Article: Codable { let title: String } struct ArticlesApiRequestable: Requestable { typealias Response = [Article] var baseURL: URL = URL(string: "https://qiita.com")! var path: String = "/api/v2/items" var httpMethod: HttpMethod = .get } func getArticles() { let requestable = ArticlesApiRequestable() let session = Session() session.send(requestable) { result in switch result { case .success(let articles): print(articles.count) case .failure(let error): print(error) } } } getArticles() 参考リンク
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CocoapodsにPrivateのフレームワークを作ってみる

はじめに 株式会社ピー・アール・オーのアドベントカレンダー3日目です。 今までiOSプロジェクトを開発時、共通で使えるユーティリティクラスを既存のプロジェクトからコピーして、新しいプロジェクト側に流用する形でした。しかし、ユーティリティクラスに修正や機能追加が入った場合、全てのプロジェクトにファイル更新を行う必要があり、効率は良くないです。 そこで、共通のユーティリティークラスをフレームワーク化し、Cocoapods経由で全てのプロジェクト内にインストールすれば今後楽になると思いました。フレームワークのソースコードは公開できない場合もあるため、CocoapodsにPrivateのフレームワークを作成する方法を調べて、作ってみました。 事前準備 Cocoapodsのインストール sudo gem install cocoapods プライベートのSpecリポジトリを作成する さて、一番最初に用意する必要があるのはSpecリポジトリです。今回はGitHub上にリポジトリを用意します。 ※リポジトリ自体はpublicにする必要がないです。ただし、チームメンバー全員リポジトリのアクセス権限を持つ必要があります。 今回はpro-specsのリポジトリを作りました。 作成したSpecリポジトリをローカルのCocoapodsに追加する Specのリポジトリを作成した後、次は作成したリポジトリをローカルのCocoapodsに追加する必要があります。 pod repo add [リポジトリ名] [リポジトリのURL] 例) pod repo add pro-specs https://github.com/lxlgarnett/pro-specs.git 追加したリポジトリが正しく追加されたかどうかをチェックする cd ~/.cocoapods/repos/[リポジトリ名] pod repo lint . 例) cd ~/.cocoapods/repos/pro-specs pod repo lint . 下のメッセージが表示すれば問題ないです。 All the specs passed validation. フレームワークリリース用podspecファイルを作成する 次はリリース用のpodspecファイルを作成します。 ※ここはすでにGitHubにリリース済みのプロジェクトが存在することを想定します。 まずは任意の場所に移動し、podspecファイルを作成します。 今回はDesktopにファイルを作成します。 cd ~/Desktop touch Sample.podspec Sample.podspecの中身は下記になります。 Pod::Spec.new do | spec | spec.name = 'Sample' spec.version = '1.0.0-alpha' spec.license = { :type => 'MIT' } spec.homepage = 'https://www.pro-japan.co.jp/' spec.authors = { 'Xiaolei Li' => 'g.rei@pro-japan.co.jp' } spec.summary = 'Qiita用サンプルフレームワーク' spec.source = { :git => 'https://github.com/lxlgarnett/sample-ios-framework.git', :tag => 'v1.0.0-alpha' } spec.source_files = 'Sample/*.swift' spec.platform = :ios, '14.0' spec.swift_version = '5.0' end spec.name: フレームワーク名、podspecのファイル名はsepc.nameと同じにする必要があります。 spec.version: バージョン spec.license: ライセンス spec.homepage: ホームページの情報 spec.authors: 作成者 spec.summary: フレームワークの説明 spec.source: ソースコードの場所 spec.source_files: 実際podに取り込むファイルのパスとファイルの形式 上記の項目は必須項目です。 作成したpodspecファイルをリモート上のSpecリポジトリにプッシュする pod repo push pro-specs ~/Desktop/sample.podspec Validating spec -> Sample (1.0.0-alpha) - NOTE | xcodebuild: note: Using new build system - NOTE | xcodebuild: note: Using codesigning identity override: - - NOTE | xcodebuild: note: Build preparation complete - NOTE | [iOS] xcodebuild: note: Planning - NOTE | [iOS] xcodebuild: note: Building targets in parallel - NOTE | [iOS] xcodebuild: warning: Skipping code signing because the target does not have an Info.plist file and one is not being generated automatically. (in target 'App' from project 'App') Updating the `pro-specs' repo Adding the spec to the `pro-specs' repo - [Add] Sample (1.0.0-alpha) Pushing the `pro-specs' repo 問題がなければ、上記のようにリポジトリに追加したのメッセージが表示されます。 公開したフレームワークを使ってみる 実際リリースしたフレームワークを利用する場合、プロジェクト内のPodfileに作成したSpecのリポジトリを追加し、フレームワーク名とバージョンを指定し、podインストールすればOKです。 source "https://github.com/lxlgarnett/pro-specs.git" target 'ProjectName' do pod 'Sample' => '1.0.0-alpha' end 最後に 共通のソースコードをフレームワーク化すれば、変更があったとしても、全てのプロジェクト内もPodfileに記載したバージョンを更新するだけで、最新のコードが利用できますので、作業の効率が良くなります。 参考サイト Cocoapodsのインストール Cocoapods Private Pods
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Cocoa][Swift]weak self

Objective-C時代にUIViewControllerでBlocksを使う場合は、以下のように、自分の過去のコードをテンプレートのようにしてコピーして使っていた。 __block ViewController * __weak blockWeakSelf = self; [[Connector sharedConnector] demoWithCompletionHandler:^() { ViewController *tempSelf = blockWeakSelf; if (! tempSelf) return; : }]; ある条件でのみ呼ばれるBlockの場合、呼ばれない可能性もあり、BlockでViewControllerのselfを強参照で保持すると、画面遷移などでViewControllerが不要になってもメモリの残り続けることになる。そこで、Blockでキャプチャするのはweak selfとし、利用時にnilなら、そのViewControllerは存在しないので、何もしないとしていた。 また、Block内で自身を参照すると、循環参照になるということで、Xcodeで警告されるようになったので、weak selfを記述するようになったと思う。 MyClass * __weak weakSelf = self; self.block = ^{ [weakSelf hoge]; weakSelf.foo = @"bar"; }; Swiftではweakに加えunownedが使え、前者はnilになる可能性があり、後者はnilにならないとなる。 プロパティの初期化では、自身はnilにならないので、以下のように遅延処理で記述できる。 class MyClass: NSObject { lasy var block: () -> Void = { [unowned self] () -> Void in self.hoge() } } また、コード中は以下のように記述できる。 self.block = { [unowned self] () -> Void in self.hoge() } 最初のViewControllerの場合のように、自身がnilになる可能性がある場合は、weakを選択ということになるのだろう。 self.block = { [weak self] () -> Void in guard let self = self else { return } self.hoge() self.foo = "bar" } 【関連情報】 Cocoa.swift Cocoa勉強会 関東 Cocoa練習帳
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む