20210911のSwiftに関する記事は9件です。

【Swift】[強制アップデート] Sirenがうまく動かない時に試すこと

はじめに 個人開発の強制アップデート機能にSirenというライブラリをいれています。 数行書くだけで、サクッと実装できるので、おすすめのライブラリです。 また、ローカライズできることや更新頻度、AlertTypeをカスタマイズできるのもいいですね。 今回、実装するにあたり、うまく動かなかったので、その原因と対処法をまとめます。 Siren について 実装方法 以下が参考になりました。 https://github.com/ArtSabintsev/Siren/blob/master/Example/Example/AppDelegate.swift https://qiita.com/ninoko1995/items/3e2e3898a49fdbb93e9f 動作検証 以下が参考になりました。 https://github.com/ArtSabintsev/Siren#testing 私は、AppStoreに既に公開していたので、ローカルのprojectファイルのバージョンを変更して検証しました。 まだリリース前の場合は、bundleIDをすでにAppStoreにあるものに変更すればいいみたいですね。 ライブラリのサンプルでは、FacebookのbundleIDを使用しています。 なぜ動かなかったのか AppStoreの配信地域を日本限定にしていたのですが、ライブラリ内部でコールしている、AppStoreAPI の countryCode のパラメータのデフォルトが unitedStates になっていたため、そもそもアプリが存在していないという判定がされていたようです。 なので以下の一行を追加したら、ちゃんと動く様になりました! Siren.shared.apiManager = APIManager(country: .japan) 全体のコードはこちら import UIKit import Siren @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Siren.shared.rulesManager = RulesManager( majorUpdateRules: Rules(promptFrequency: .immediately, forAlertType: .force), minorUpdateRules: Rules(promptFrequency: .immediately, forAlertType: .option), patchUpdateRules: Rules(promptFrequency: .immediately, forAlertType: .skip), ) Siren.shared.apiManager = APIManager(country: .japan) // !! この1行を追加 Siren.shared.wail() return true } } 以上です!ご参考になれば幸いです! 動作環境 Xcode 12.5 Swift 5
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【強制アップデート】Sirenがうまく動かない時に試すこと

はじめに 個人開発の強制アップデート機能にSirenというライブラリをいれています。 数行書くだけで、サクッと実装できるので、おすすめのライブラリです。 また、ローカライズできることや更新頻度、AlertTypeをカスタマイズできるのもいいですね。 今回、実装するにあたり、うまく動かなかったので、その原因と対処法をまとめます。 Siren について 実装方法 以下が参考になりました。 https://github.com/ArtSabintsev/Siren/blob/master/Example/Example/AppDelegate.swift https://qiita.com/ninoko1995/items/3e2e3898a49fdbb93e9f 動作検証 以下が参考になりました。 https://github.com/ArtSabintsev/Siren#testing 私は、AppStoreに既に公開していたので、ローカルのprojectファイルのバージョンを変更して検証しました。 まだリリース前の場合は、bundleIDをすでにAppStoreにあるものに変更すればいいみたいですね。 ライブラリのサンプルでは、FacebookのbundleIDを使用しています。 なぜ動かなかったのか AppStoreの配信地域を日本限定にしていたのですが、ライブラリ内部でコールしている、AppStoreAPI の countryCode のパラメータのデフォルトが unitedStates になっていたため、そもそもアプリが存在していないという判定がされていたようです。 なので以下の一行を追加したら、ちゃんと動く様になりました! Siren.shared.apiManager = APIManager(country: .japan) 全体のコードはこちら import UIKit import Siren @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Siren.shared.rulesManager = RulesManager( majorUpdateRules: Rules(promptFrequency: .immediately, forAlertType: .force), minorUpdateRules: Rules(promptFrequency: .immediately, forAlertType: .option), patchUpdateRules: Rules(promptFrequency: .immediately, forAlertType: .skip), ) Siren.shared.apiManager = APIManager(country: .japan) // !! この1行を追加 Siren.shared.wail() return true } } 以上です!ご参考になれば幸いです! 動作環境 Xcode 12.5 Swift 5
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面をスワイプした事を検知(UISwipeGestureRecognizer)

今回の内容  画面をスワイプした事を検知 コードと簡単解説 .directionには、.down .left .right .upから選択して使用します。 .downは下に向けてスワイプしたことを検知します。 .leftは左側に向けてスワイプしたことを検知します。 .rightは右側に向けてスワイプしたことを検知します。 .upは上に向けてスワイプしたことを検知します。 4つ全てを使用する場合は、4つ分インスタンスを作成します。 .directionを設定しないで使用すると、右側に向けてスワイプした時に検知しました。 override func viewDidLoad() { super.viewDidLoad() let swipeDetection = UISwipeGestureRecognizer(target: self, action: #selector(screenSwipe)) swipeDetection.direction = .down view.addGestureRecognizer(swipeDetection) } @objc func screenSwipe(){ //スワイプを検知した時の処理 } 終わり ご指摘、ご質問などありましたら、コメントまでお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】EUC-JPでURLエンコーディングする

はじめに クエリを含むURLを作成したい時には、URLエンコーディングを行う必要があると思います。 EUC-JPでエンコードする記事はあまり見かけないのでまとめます。(今どき、EUC-JP使っているサーバーなんてほぼないと思いますが。。。) ご参考になればと思います! 実装してみる 今回は String の Extension で実装します。 import Foundation extension String { var encodedByEUCJP: String { // エンコーディングしない文字列をキャラクタセットとして定義 let allowedCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~") // エンコーディングを定義する let eucjpEncoding = String.Encoding(rawValue:CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.EUC_JP.rawValue))) // Data型にする let data = self.data(using: eucjpEncoding, allowLossyConversion: true) ?? Data() // URLエンコードしていく let urlEncoded = data.map { byte -> String in if allowedCharacters.contains(UnicodeScalar(byte)) { return String(UnicodeScalar(byte)) } else if byte == UInt8(ascii: " ") { return "+" } else { return String(format: "%%%02X", byte) } }.joined() return urlEncoded } } 試してみる 無事、EUC-JPでURLエンコーディングできました! print("テスト".encodedByEUCJP) // %A5%C6%A5%B9%A5%C8
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EUC-JPでURLエンコーディングする

はじめに クエリを含むURLを作成したい時には、URLエンコーディングを行う必要があると思います。 EUC-JPでエンコードする記事はあまり見かけないのでまとめます。(今どき、EUC-JP使っているサーバーなんてほぼないと思いますが。。。) ご参考になればと思います! 実装してみる 今回は String の Extension で実装します。 import Foundation extension String { var encodedByEUCJP: String { // エンコーディングしない文字列をキャラクタセットとして定義 let allowedCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~") // エンコーディングを定義する let eucjpEncoding = String.Encoding(rawValue:CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.EUC_JP.rawValue))) // Data型にする let data = self.data(using: eucjpEncoding, allowLossyConversion: true) ?? Data() // URLエンコードしていく let urlEncoded = data.map { byte -> String in if allowedCharacters.contains(UnicodeScalar(byte)) { return String(UnicodeScalar(byte)) } else if byte == UInt8(ascii: " ") { return "+" } else { return String(format: "%%%02X", byte) } }.joined() return urlEncoded } } 試してみる 無事、EUC-JPでURLエンコーディングできました! print("テスト".encodedByEUCJP) // %A5%C6%A5%B9%A5%C8
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】CombineとUIKitを連携させる(サンプルコードあり)

CombineとUIKitを連携させるいい感じのサンプルを作りました。 Combineを使うことで各コンポーネントのアクションの検知をスッキリ書くかことができて最高に気持ちいいです。 参考にしてもらえると嬉しいです。 サンプルコード asa08/combine-sample Combine+UIControl UIControlには以下のコンポーネントがあります。 これらのアクションをCombineでsinkできるようにします。 UIButton UITextField UIPageControl UISwitch etc... よく使うUIButtonとUITextFieldを紹介します。 CombineのExtensionを作成する UIControlのExtensionを作成します。実装はこちらを拝借しました。 サンプルコードの箇所 Combine+UIControl.swift import Combine import UIKit public protocol CombineCompatible {} public extension UIControl { final class Subscription<SubscriberType: Subscriber, Control: UIControl>: Combine.Subscription where SubscriberType.Input == Control { private var subscriber: SubscriberType? private let input: Control public init(subscriber: SubscriberType, input: Control, event: UIControl.Event) { self.subscriber = subscriber self.input = input input.addTarget(self, action: #selector(eventHandler), for: event) } public func request(_ demand: Subscribers.Demand) {} public func cancel() { subscriber = nil } @objc private func eventHandler() { _ = subscriber?.receive(input) } } struct Publisher<Output: UIControl>: Combine.Publisher { public typealias Output = Output public typealias Failure = Never let output: Output let event: UIControl.Event public init(output: Output, event: UIControl.Event) { self.output = output self.event = event } public func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { let subscription = Subscription(subscriber: subscriber, input: output, event: event) subscriber.receive(subscription: subscription) } } } extension UIControl: CombineCompatible {} public extension CombineCompatible where Self: UIControl { func publisher(for event: UIControl.Event) -> UIControl.Publisher<UIControl> { .init(output: self, event: event) } } UIButton+Combine UIButtonのタップを検知する サンプルコードの箇所 UIButton.swift var cancellables = Set<AnyCancellable>() let uibutton = UIButton() uibutton.publisher(for: .touchUpInside).sink(receiveValue: { _ in print("button tapped!!") }).store(in: &cancellables) UITextField+Combine UITextFieldのテキストの変更を検知する サンプルコードの箇所 UITextField.swift // テキストの変更を検知してLabelにassignする var cancellables = Set<AnyCancellable>() let label = UILabel() let uitextfield = UITextField() uitextfield.publisher(for: .allEditingEvents) .map{ _ in self.uitextfield.text } .assign(to: \.text, on: label) .store(in: &cancellables) Combine+UIViewのGesture UIViewのGestureをCombineでsinkできるようにします。 CombineのExtensionを作成する 実装はこちらを拝借しました。 サンプルコードの箇所 Combine+TapGesture.swift import UIKit import Combine enum GestureType { case tap(UITapGestureRecognizer = .init()) case swipe(UISwipeGestureRecognizer = .init()) case longPress(UILongPressGestureRecognizer = .init()) case pan(UIPanGestureRecognizer = .init()) case pinch(UIPinchGestureRecognizer = .init()) case edge(UIScreenEdgePanGestureRecognizer = .init()) func get() -> UIGestureRecognizer { switch self { case let .tap(tapGesture): return tapGesture case let .swipe(swipeGesture): return swipeGesture case let .longPress(longPressGesture): return longPressGesture case let .pan(panGesture): return panGesture case let .pinch(pinchGesture): return pinchGesture case let .edge(edgePanGesture): return edgePanGesture } } } struct GesturePublisher: Publisher { typealias Output = GestureType typealias Failure = Never private let view: UIView private let gestureType: GestureType init(view: UIView, gestureType: GestureType) { self.view = view self.gestureType = gestureType } func receive<S>(subscriber: S) where S: Subscriber, GesturePublisher.Failure == S.Failure, GesturePublisher.Output == S.Input { let subscription = GestureSubscription( subscriber: subscriber, view: view, gestureType: gestureType ) subscriber.receive(subscription: subscription) } } class GestureSubscription<S: Subscriber>: Subscription where S.Input == GestureType, S.Failure == Never { private var subscriber: S? private var gestureType: GestureType private var view: UIView init(subscriber: S, view: UIView, gestureType: GestureType) { self.subscriber = subscriber self.view = view self.gestureType = gestureType configureGesture(gestureType) } private func configureGesture(_ gestureType: GestureType) { let gesture = gestureType.get() gesture.addTarget(self, action: #selector(handler)) view.addGestureRecognizer(gesture) } func request(_ demand: Subscribers.Demand) {} func cancel() { subscriber = nil } @objc private func handler() { _ = subscriber?.receive(gestureType) } } extension UIView { func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher { .init(view: self, gestureType: gestureType) } } UIView+Combine UIViewのタップを検知する サンプルコードの箇所 UIView.swift var cancellables = Set<AnyCancellable>() let uiview = UIView() uiview.gesture().sink(receiveValue: { _ in print("view tapped!!") }).store(in: &cancellables) UIVIewのスワイプを検知する UIView.swift var cancellables = Set<AnyCancellable>() let uiview = UIView() uiview.gesture(.swipe()).sink { _ in print("view swiped!!") }.store(in: &cancellables) UITextView+Combine UITextFieldはUIControlを継承していますがUITextViewは違うので、こちらは別途作成が必要です。 今回はテキストの変更の検知だけ紹介します。 CombineのExtensionを作成する サンプルコードの箇所 Combine+UITextView.swift import UIKit import Combine extension UITextView { func textPublisher() -> AnyPublisher<String, Never> { NotificationCenter.default .publisher(for: UITextView.textDidChangeNotification, object: self) .map { ($0.object as? UITextView)?.text ?? "" } .eraseToAnyPublisher() } } UITextView+Combine テキストの変更を検知する サンプルコードの箇所 UITextViw.swift textView.textPublisher().sink(receiveValue: { text in // 変更後のテキストはtext print("text canged!!") }).store(in: &cancellables)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

@IBDesignable, @IBInspectableなカスタムビューを実装する

はじめに どの画面に何のパーツがあるのか一目でわかるようにカスタムビューをIB上に表示したい時がありました。 @IBDesignable, @IBInspectableを使った実現方法を紹介します。 環境 Xcode 12.5.1 macOS Big Sur 11.5.2 完成形 完成すると動画のように変更がリアルタイムで確認できます。 大まかな実装手順 UIViewを継承したCocoa Touch Class(CustomView.swift)を作成する xibファイル(CustomView.xib)を作成する File's ownerでswiftファイルとxibファイルを紐付ける @IBDesignable, @IBInspectableを使ってCustomView.swiftを実装する 次にCustomView.swiftの実装について詳しく見ていきます。 CustomView.swift CustomViewでは受け取った文字列をUILabelに反映します。 文字列を画像のようにインスペクタ上で変更できるよう@IBInpspectableを宣言します。 また、カスタムビューの変更をIB上に表示するためにCustomViewに@IBDesignableを設定します。 CustomView.swift @IBDesignable class CustomView: UIView { @IBOutlet weak var label: UILabel! @IBInspectable var labelString: String = "" override init(frame: CGRect) { super.init(frame: frame) loadNib() } required init?(coder: NSCoder) { super.init(coder: coder) loadNib() } override func layoutSubviews() { super.layoutSubviews() setupLabel() } private func loadNib() { guard let view = Bundle(for: type(of: self)).loadNibNamed("CustomView", owner: self, options: nil)?.first as? UIView else { return } view.frame = bounds addSubview(view) } private func setupLabel() { label.text = labelString } } 最後に カスタムビューがIB上で見えることで、どこに何があるか一目で分かります。 後からプロジェクトに入った方も実装を進めやすいと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】Metal Best Practicesの解説(9) ロードと保存

Metal Best Practicesは、iOS/MacOS/tvOSのAPIであるMetalを用いた設計のベストプラクティスガイドです。 本稿では、何回かに分けてこのガイドを読み解き、コード上での実験を交えて解説していきます。 読んでそのまま理解できそうなところは飛ばしますので、原文を読みながら原文のガイドとしてご利用下さい。 また、iOSの記事なので他のOS(MacOS, tvOS)についての記載は割愛します。 他の記事の一覧は、初回記事よりご覧下さい。 Load and Store Actions(ロードと保存) ベストプラクティス:レンダーターゲット(レンダー先)に適切なロードおよびストアアクションを設定します。 レンダリングパスの開始時のロードアクションと、終了時のストアアクションは、不要なコストを回避するために、適切なアクションを選択しましょうという話。 ロードアクション、ストアアクションの指定というのは要するに、今回レンダリングする対象にレンダリング前に前回のフレームのコンテンツを読み込んだり、クリアするかどうか(ロードアクション)、レンダリング後にコンテンツを保存するかどうか(ストアアクション)を指定する、ということです。 具体的には、ロードアクションでは、レンダーターゲットにある以前のコンテンツを、気にしない(.dontCare)/クリアする(.clear)/以前のコンテンツをロードする(.load)、のどれかを選択します。 ストアアクションでは、レンダリングパスの処理が終わったあと、レンダーターゲットの内容を、保持しない(.dontCare)/保持する(.store)/マルチサンプルの解決をして保持する(. storeAndMultisampleResolve)/マルチサンプルの解決をして破棄する(.multisampleResolve)する、のどれかを選択します。 通常、ドローアブルにレンダリングするときの内容を保持する必要があるためストアアクションは通常.storeになります。depthやstencilのレンダーターゲットの場合は、レンダリング中は値が必要ですが、レンダリング後には不要になるので一般的には.dontCareを使います。 なお、複数のレンダリングパスで使用されるレンダーターゲットは、レンダリングパス間のストアアクション、ロードアクションの組み合わせを気にする必要があります。 コードで確認してみる コードで確認してみます。 こちらのサンプルコードを改変して、検証します。 ロードアクションに.clear、ストアアクションに.storeを指定した場合 この指定をした場合、レンダリング前に以前のコンテンツはクリアします。レンダリング後は内容を保持しします。 まず表示するパーティクル数は負荷をかけるために100万にしておきます。 ParticleMetalView.swift func draw(in view: MTKView) { // 略 // パーティクルの数を100万にする renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1000000) // 略 } またレンダリングをしない領域も作りたいので、パーティクルは画面中央だけに表示するようにします。 RandomParticleShader.metal vertex ColorInOut randomParticleVertexShader( const device float4 *positions [[ buffer(0)]], const device float2 *texCoords [[ buffer(1) ]], constant Uniforms &uniforms [[buffer(2)]], uint vid [[ vertex_id ]], uint iid [[ instance_id ]] ) { ColorInOut out; float t = uniforms.time; float4 pos = positions[vid]; float4 converted = flowDownParticle(pos, iid, t); converted.x /= 2; // 追加 out.position = converted; out.texCoords = texCoords[vid]; out.instanceId = iid; return out; } そして今回のテーマである、loadActionを.clearに、storeActionを.storeにします。 ParticleMetalView.swift func draw(in view: MTKView) { guard let drawable = view.currentDrawable else {return} let commandBuffer = metalCommandQueue.makeCommandBuffer()! renderPassDescriptor.colorAttachments[0].texture = drawable.texture renderPassDescriptor.colorAttachments[0].loadAction = .clear // 指定する renderPassDescriptor.colorAttachments[0].storeAction = .store // 指定する renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.8, 0.7, 0.1, 1.0) // 略 // パーティクルの数を100万にする renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1000000) 実行してみると、実機(iPhone 11)で、49〜50FPS出ていました。 実行イメージは次のようになります。レンダリングしていない領域は、clearColorに指定した色になっています。 ロードアクションに.dontCare、ストアアクションに.storeを指定した場合 この指定の場合、レンダリング前に以前のコンテンツをロードすることもクリアすることもありません。レンダリング後は内容を保持しします。 先程の部分を次のように書き換えます。 ParticleMetalView.swift renderPassDescriptor.colorAttachments[0].loadAction = .dontCare renderPassDescriptor.colorAttachments[0].storeAction = .store 実行してみると、50〜51FPS出ています。少し早くなりましたね。 実行イメージは、レンダリングしていないは領域が黒くなりました。なお、このシェーダーは100万パーティクルをレンダリングしたあと描画をやめますが、loadActionが.dontCareの場合、描画をやめた後は未定義になるため、途中からこんな感じに画面が崩れます。 ロードアクションに.dontCare、ストアアクションに.dontCareを指定した場合 この指定の場合、レンダリング前に以前のコンテンツをロードすることもクリアしないし、レンダリング後も内容を保持しません。 先程の部分を次のように書き換えます。 ParticleMetalView.swift renderPassDescriptor.colorAttachments[0].loadAction = .dontCare renderPassDescriptor.colorAttachments[0].storeAction = .store 実行すると52〜53FPSになりました。さらに早くなりましたね。 ただ、この指定だとレンダリング後にも内容が保持されないので、画面の状態が未定義になりこのようなちらつく画面になってしまいました。 ※チラつきが早いと目に悪いのでFPSを落とした動画にしています 結論 わずかな差でしたが、ロードアクション、ストアアクションで余計な処理をしないことで、パフォーマンスが上がることが確認できました。 最後に iOSを使った3D処理やAR、ML、音声処理などの作品やサンプル、技術情報を発信しています。 作品ができたらTwitterで発信していきますのでフォローをお願いします? Twitterは作品や記事のリンクを貼っています。 https://twitter.com/jugemjugemjugem Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。 https://qiita.com/TokyoYoshida Noteでは、連載記事を書いています。 https://note.com/tokyoyoshida Zennは機械学習が多めです。 https://zenn.dev/tokyoyoshida
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GithubAPIでリポジトリをカスタムセルで、一覧表示させてみた

初めに SwiftでGithubAPIを使ってリポジトリ一覧アプリを作ってみたいと思います。 初心者にもわかりやすく、AutoLayoutの設定、デザインパターン、コードの可読性もしっかり守っているので、APIの入門記事としてはぴったりかなと。 まず完成形はこちら! UIの設計 このように配置していきます。 DefaultであるMain.storyboardの名前をSearchViewController.swiftに変更してください。 もしstoryboardの変更でエラーが出たら、こちらを参照してください。 制約をつけていきます。 SearchViewController,RepositoryCellを作り、IBOutlet,IBAction接続します。 SearchViewController.swift import UIKit class SearchViewController: UITableViewController { @IBOutlet private weak var searchBar: UISearchBar!{ didSet { searchBar.placeholder = "リポジトリを検索できるよ!" searchBar.delegate = self } } } RepositoryCell.swift import UIKit class RepositoryCell: UITableViewCell { @IBOutlet private weak var ownerImageView: UIImageView!{ didSet { ownerImageView.layer.cornerRadius = 10 ownerImageView.clipsToBounds = true } } @IBOutlet private weak var repositoryNameLabel: UILabel!{ didSet{ repositoryNameLabel.textColor = UIColor.link } } @IBOutlet private weak var starImageView: UIImageView!{ didSet { starImageView.setImage(systemName: "star", tintColor: UIColor.systemGray) } } @IBOutlet private weak var ownerNameLabel: UILabel! @IBOutlet private weak var repositoryDescriptionLabel: UILabel! @IBOutlet private weak var starCountLabel: UILabel! @IBOutlet private weak var languageLabel: UILabel! static let cellIdentifier = String(describing: RepositoryCell.self) } 全体設計 UIができた後に、今回のアプリの設計を行なっていく。 APIの取得 まず、APIの取得からやっていきたいと思います。 GitHubAPIを使います。 今回は,https://api.github.com/search/repositories?q=\(text)を使っていきます。 こちらでこのように(https://api.github.com/search/repositories?q=Swift)APIを叩くと、JSONデータを変換してくれます。 これらのデータをうまく使い今回はアプリを作成していきます。 GitHubAPI 今回のAPIにおいてのロジックを管理するGitHubAPIを書いていきます。 GitHubAPI.swift import Foundation class GitHubAPI { private static var task: URLSessionTask? enum FetchRepositoryError: Error { case wrong case network case parse } static func fetchRepository(text: String, completionHandler: @escaping (Result<[Repository], FetchRepositoryError>) -> Void) { if !text.isEmpty { let urlString = "https://api.github.com/search/repositories?q=\(text)".addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! guard let url = URL(string: urlString) else { completionHandler(.failure(FetchRepositoryError.wrong)) return } let task = URLSession.shared.dataTask(with: url) { (data, res, err) in if err != nil { completionHandler(.failure(FetchRepositoryError.network)) return } guard let safeData = data else {return} let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let decodedData = try decoder.decode(Repositories.self, from: safeData) completionHandler(.success(decodedData.items)) } catch { completionHandler(.failure(FetchRepositoryError.parse)) } } task.resume() } } static func taskCancel() { task?.cancel() } } Repository レスポンスしたデータをデコードするためRepositoryを作ります。 Repository.swift import Foundation import UIKit struct Repositories: Codable { let items: [Repository] } struct Repository: Codable { let name: String let fullName: String let language: String? let stargazersCount: Int let watchersCount: Int let forksCount: Int let openIssuesCount: Int let description: String? let owner: Owner var avatarImageUrl: URL? { return URL(string: owner.avatarUrl) } } struct Owner: Codable { let avatarUrl: String let login: String } SearchViewController 取得したデータをViewに反映させる、またTableView,SearchBarの操作のためにSearchViewControllerを作っていきます。 その前にロード処理のために便利なJGProgressHUDというライブラリを使いたいと思います。 JGProgressHUDの詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。 SearchViewController.swift import UIKit import JGProgressHUD class SearchViewController: UITableViewController { @IBOutlet private weak var searchBar: UISearchBar!{ didSet { searchBar.placeholder = "リポジトリを検索できるよ!" searchBar.delegate = self } } private var repositories: [Repository] = [] override func viewDidLoad() { super.viewDidLoad() let nib = UINib(nibName: RepositoryCell.cellIdentifier, bundle: nil) tableView.register(nib, forCellReuseIdentifier: RepositoryCell.cellIdentifier) tableView.rowHeight = UITableView.automaticDimension } private func showAlert(title: String, message: String = "") -> UIAlertController { let alert: UIAlertController = UIAlertController(title: title, message : message, preferredStyle: UIAlertController.Style.alert) let defaultAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.default) alert.addAction(defaultAction) return alert } private func wrongError() -> UIAlertController { return showAlert(title: "不正なワードの入力", message: "検索ワードの確認を行ってください") } private func networkError() -> UIAlertController { return showAlert(title: "インターネットの非接続", message: "接続状況の確認を行ってください") } private func parseError() -> UIAlertController { return showAlert(title: "データの解析に失敗しました") } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return repositories.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: RepositoryCell.cellIdentifier, for: indexPath) as! RepositoryCell let repository = repositories[indexPath.row] cell.configure(repository: repository) return cell } } //MARK: - UISearchBarDelegate extension SearchViewController:UISearchBarDelegate{ func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { return true } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { GitHubAPI.taskCancel() } func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { guard !(searchBar.text?.isEmpty ?? true) else { return } searchBar.resignFirstResponder() let progressHUD = JGProgressHUD() progressHUD.show(in: self.view) if let word = searchBar.text{ GitHubAPI.fetchRepository(text: word) { result in DispatchQueue.main.async { progressHUD.dismiss() } switch result { case .success(let items): self.repositories = items DispatchQueue.main.async { self.tableView.reloadData() } case .failure(let error): DispatchQueue.main.async { switch error { case .wrong : let alert = self.wrongError() self.present(alert, animated: true, completion: nil) return case .network: let alert = self.networkError() self.present(alert, animated: true, completion: nil) return case .parse: let alert = self.parseError() self.present(alert, animated: true, completion: nil) return } } } } } return } } RepositoryCell tableViewCellの配置を行うRepositoryCellを作っていきます。 その前に画像のキャッシュのために便利なSDWebImageというライブラリを使いたいと思います。 SDWebImageの詳しい説明、導入の仕方などはこれらの記事を見るとわかると思います。 RepositoryCell.swift import UIKit import SDWebImage class RepositoryCell: UITableViewCell { @IBOutlet private weak var ownerImageView: UIImageView!{ didSet { ownerImageView.layer.cornerRadius = 10 ownerImageView.clipsToBounds = true } } @IBOutlet private weak var repositoryNameLabel: UILabel!{ didSet{ repositoryNameLabel.textColor = UIColor.link } } @IBOutlet private weak var starImageView: UIImageView!{ didSet { starImageView.setImage(systemName: "star", tintColor: UIColor.systemGray) } } @IBOutlet private weak var ownerNameLabel: UILabel! @IBOutlet private weak var repositoryDescriptionLabel: UILabel! @IBOutlet private weak var starCountLabel: UILabel! @IBOutlet private weak var languageLabel: UILabel! static let cellIdentifier = String(describing: RepositoryCell.self) func configure(repository: Repository) { ownerNameLabel.text = repository.owner.login repositoryNameLabel.text = repository.fullName if let url = repository.avatarImageUrl { ownerImageView.sd_setImage(with: url, completed: nil) } else { ownerImageView.image = nil } repositoryDescriptionLabel.text = repository.description ?? "" starCountLabel.text = "\(repository.stargazersCount)" languageLabel.text = repository.language accessoryType = .disclosureIndicator } }  SFSymbols UIImageViewの拡張クラスを作っていきます。 SFSymbols.swift import UIKit extension UIImageView { func setImage(systemName: String, tintColor: UIColor) { self.image = UIImage(systemName: systemName) self.tintColor = tintColor } } 終わりに 以上でこのようなアプリができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む