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

[Swift]型のプロパティについて

Swiftの型

Swiftの型は、クラス、構造体、列挙型として定義できます。
クラス、構造体、列挙型の3つには、メソッドやプロパティなどの共通の要素が用意されています。

宣言方法

struct 構造体名{
   構造体の定義
}
class クラス名{
   クラスの定義
}
enum 列挙型名{
   列挙型の定義
}

インスタン化の方法

型をインスタンス化するには、以下のようにします。
その時同時にイニシャライザが呼び出されます。イニシャライザは、インスタンス化する時に呼びだれるメソッドです。

型名()

プロパティ

型に紐づいた値のことをプロパティと呼びます。
より詳しくいうと、型の中で定義された変数をプロパティと言います。

定義方法

プロパティはvarやletで定義します。

struct 構造体名{
   //再代入可能なプロパティ
   var プロパティ名:プロパティの型=
   //再代入不可能なプロパティ
   let プロパティ名:プロパティの型=
}

プロパティの整合性を保つために、インスタンス化するまでに全てのプロパティの型を決める必要があるのでプロパティに初期値を代入する必要があります。
構造体の中でプロパティを初期化するか、イニシャライザで初期化するかいづれかの方法でプロパティに値を渡します。

プロパティの分類

プロパティには2種類あります。
1.インスタンスに紐づくプロパティ(インスタンスプロパティ)
2.型に紐づくプロパティ(スタティックプロパティ)

インスタンスプロパティ

struct Area{
    var tate=10
    var yoko=10
}
var A_1=Area()//インスタンス化
var A_2=Area()//インスタンス化
A_1.tate=3
A_2.tate=7
print(A_1.tate)//3
print(A_2.tate)//7

インスタンス化とは実物にするということです。
つまり、A_1とA_2という二つの物体を生成したことになります。
そのA_1とA_2の縦を変更したので、それぞれ3と7になります。
じゃあ、元々の縦を変更したい時にはどうすれば良いでしょうか。
その時は、staticキーワードを使います

スタティックプロパティ

struct Area{
    static var tate=10
    var yoko=10
}
var A_1=Area()
var A_2=Area()
Area.tate=3
A_1.tate=7//エラー
print(Area.tate)//3

A_1やA_2からtateにアクセスすることはできません。
そのかわり、型に直接3を代入することでtateを10から3に変更できます。

スタティックプロパティにはイニシャライザみたいな初期化するメソッドがないため宣言文のなかで初期化する必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Combine + APIKit で複数リクエストを直列で実行するサンプル (ついでに並列も)

title.png

Combine.frameworkを体験する取っ掛かりとして
APIKitのRequestにPublisherを生やして
複数の非同期通信を直列・並列で叩くサンプルを実装してみました。

まだまだCombineニュービーですのでオススメの方法や
間違い等有りましたらご指摘頂けると幸いです??‍♂️

環境

  • Xcode 12.1
  • APIKit 5.1.0
  • SwiftUI

サンプルコード

https://github.com/SatoshiN303/RequestsWithCombine

プロジェクト構成

APIKitにPublisherを生やす実装方法については
Developers.IO さんの以下の記事が
非常にわかりやすかったのでほぼコピさせていただきました。
[iOS 13] SwiftUI + Combine + APIKitでインクリメンタルサーチ

今回のサンプルコードでは複数の通信を叩くために
GitHub REST APIを使う形に変更しています。

スクリーンショット 2021-01-21 16.00.27.png

処理の流れ

主な登場人物は上記赤枠の3つです。
処理の流れは以下になります。

  1. ContentViewUISearchBarの入力を受け取る
  2. SampleModelでリクエストを作成して直列の通信を開始する
  3. 最初に/search/repositoriesを叩く
  4. 上記のレスポンスを利用して/repos/{owner}/{repositry}を叩く
  5. 通知された結果を表示する

直列で叩く(Publisher)

最初に/search/repositories を叩き、レスポンスを利用して
/repos/{owner}/{repositry} を叩いてみます。
正直最初のリクエストで欲しいデータは取得できますが学びということで。

        // `/search/repositories` のリクエスト作成
        let searchRepositories = SearchRepositoriesRequest(query: searchText).publisher.eraseToAnyPublisher()
        self.requestCancellable = searchRepositories
            // レスポンスはSearchRepositoriesResponse型なのでitems.firstをとりあえず取得
            .compactMap { $0.items.first }
            // 上記レスポンスを利用して GetRepositoyRequest を作成
            .flatMap { (repository) -> AnyPublisher<GetRepositoyRequest.Response, Error> in
                /* 処理を繋げるとコンパイラが型がネストして解釈して複雑な型になるため
                 * eraseToAnyPublisher()で型消去した
                 * AnyPublisher<GetRepositoyRequest.Response, Error> を返す
                 */ 
                GetRepositoyRequest(owner: repository.owner.login, repositry: repository.name).publisher.eraseToAnyPublisher()
            }
            .receive(on: DispatchQueue.main)
            .sink { (completion) in
                switch completion {
                case .finished:
                    debugPrint("request finished")
                case let .failure(error):
                    debugPrint("request failed : \(error)")
                }
            } receiveValue: { [weak self] repository in
                // `/repos/{owner}/{repositry}` のレスポンスをContentViewに渡す
                self?.items = [repository]
            }

無事直列で叩けてる模様です。
スクリーンショット 2021-01-21 17.18.37.png

補足リンク eraseToAnyPublisher()での型消去について

https://developer.apple.com/documentation/combine/publisher/3241548-erasetoanypublisher

Futureを使って直列で叩く

FutureはSubscribeされなくても
インスタンスが生成されたタイミングで即時実行されるので
とりあえずクロージャーで返すメソッドを作って処理を繋げてみました。
こちらも本来であれば再利用のためにAPIKitに生やすのが望ましいでしょうか。

        // MARK: - Futureを使って直列で叩く

        self.requestCancellable = fetchRepos(query: searchText)
            .compactMap { $0.items.first }
            .flatMap { [unowned self] repository in
                self.fetchRepository(owner: repository.owner.login, repo: repository.name)
            }.sink { (completion) in
                switch completion {
                case .finished:
                    debugPrint("request finished")
                case let .failure(error):
                    debugPrint("request failed : \(error)")
                }
            } receiveValue: { [weak self] repository in
                self?.items = [repository]
            }


    // MARK: - 各リクエストの生成処理

    // ※Futureは即時実行される => インスタンスが生成されたタイミングでSubscribeをしなくても処理が走る
    private func fetchRepos(query: String) -> Future<SearchRepositoriesRequest.Response, Error> {
        return Future<SearchRepositoriesRequest.Response, Error> { promise in
            Session.send(SearchRepositoriesRequest(query: query), callbackQueue: .main) { (result) in
                switch result {
                case .success(let res):
                    // 処理成功
                    promise(.success(res))
                case .failure(let error):
                    // 処理失敗
                    promise(.failure(error))
                }
            }
        }
    }

    // ※Futureは即時実行される => インスタンスが生成されたタイミングでSubscribeをしなくても処理が走る
    private func fetchRepository(owner: String, repo: String) -> Future<GetRepositoyRequest.Response, Error> {
        return Future<GetRepositoyRequest.Response, Error> { promise in
            Session.send(GetRepositoyRequest(owner: owner, repositry: repo), callbackQueue: .main) { (result) in
                switch result {
                case .success(let res):
                    // 処理成功
                    promise(.success(res))
                case .failure(let error):
                    // 処理失敗
                    promise(.failure(error))
                }
            }
        }
    }

こちらも無事に直列で叩けてる模様です。
スクリーンショット 2021-01-21 17.19.50.png

ついでに並列でも叩いてみる

Publishers.Zipに渡せば並列で叩けるので
/search/repositories/search/usersを同じクエリで叩いてみます。

        let searchRepositories = SearchRepositoriesRequest(query: searchText).publisher.eraseToAnyPublisher()
        let searchUsers = SearchUsersRequest(query: searchText).publisher.eraseToAnyPublisher()

        self.requestCancellable = Publishers.Zip(searchRepositories, searchUsers)
            .receive(on: DispatchQueue.main)
            .sink { _ in } receiveValue: { (repos, users) in
                print(repos)
                print(users)
            }

無事叩けてました。
スクリーンショット 2021-01-21 17.21.01.png

ついでに補足比較: RxSwiftで直列で叩く場合

RxSwiftの場合はSessionにObservableを生やしてflatMapで繋ぐ感じでしょうか。

extension APIKit.Session {
    func rx_sendRequest<T: Request>(request: T) -> Observable<T.Response> {
        return Observable.create { observer in
            let task = self.send(request) { result in
                switch result {
                case let .success(res):
                    observer.on(.next(res))
                    observer.on(.completed)
                case let .failure(err):
                    observer.onError(err)
                }
            }
            return Disposables.create {
                task?.cancel()
            }
        }
    }
}

   // Observableで包んで返す

    func searchRepositories(request: SearchRepositoriesRequest) -> Observable<SearchRepositoriesRequest.Response> {
        return Session.rx_sendRequest(request: request)
    }

    func getRepository(request: GetRepositoryRequest) -> Observable<GetRepositoryRequest.Response> {
        return Session.rx_sendRequest(request: request)
    }

    // もろもろ省略

    let observable = searchRepositories(request: searchRepositoriesRequest)
            .flatMap { [unowned self] repos -> Observable<GetRepositoryRequest.Response> in
                let item = repos.items.first //とりあえず成功してアンラップできてるものとみなす
                let getRepositoryRequest = getRepositoryRequest(owner: item.owner.login, repo: item.name)
                return self.getRepository(request: getRepositoryRequest)
            }.もろもろ続く

参考にさせて頂きました。ありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift 遅延実行

はじめに

この記事では、Swiftの遅延実行について簡潔にまとめました。

参考書籍

この記事は以下の情報を参考にして執筆しました。

遅延実行とは

遅延実行とは、特定の文をそれが記述されている箇所よりも後で実行すること。

defer文 - スコープ退出時の処理

defer文において、{}内のソースコードはdefer文が記述されているスコープの退出時に実行される。
リソースの解放など、その後の実行フローの内容にかかわらず、スコープの退出時に確実に実行されて欲しい処理の記述に利用する

defer {
defer文が記述されているスコープの退出時に実行される文
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

結局ScrollViewのConstraintってどうつければいいの?

  1. UIScrollViewの上下左右をUIViewControllerのViewに合わせる
  2. ContentViewの上下左右をContent Layout Guideに合わせる
  3. ContentViewの横幅をFrame Layout Guideに合わせる(Equal Widths)
  4. ContentViewにオブジェクトを追加して高さを確定させる

名前の通り、Content Layout GuideはUIScrollViewの中身、Frame Layout GuideはUIScrollViewのフレーム領域を示します。
https://qiita.com/owen/items/2fab4a4482834b95e349

つまづきやすかったこと

ContentView は自分で UIView を追加して作る

UIViewで囲むのがセオリーとは思うものの、ちょっとAppleさん不親切‥

Content Layout Guide の制約に注意

ContentView に Content Layout Guide の上下左右の制約をかけるとき、ちゃんと画面いっぱいに伸ばしてCtrlで制約を引っ張ってもなぜか+315や+812とズレがつく。見た目では気づきにくいが、これを0に修正しないとスクロールしないので注意。
スクリーンショット 2020-12-31 18.48.43.png
スクリーンショット 2021-01-21 10.35.31.png

高さを確定させよ

中のコンテンツの一番下の要素にContentView.bottomとのconstraintを付けないと高さ確定しないので怒られる。

よくある間違い?

ContentViewのbottomをFrame Layout Guideに合わせてしまう

正しい設定では中の要素が少ない時に下側がスカスカに空いてしまう場合がある。それを埋めようとContentViewのBottomをFrameに合わせると、FrameはiPhoneの上下左右の見えてる部分らしく、本来もっと下に伸びるべきContentViewのBottomがiPhoneの底で止まってスクロールしなくなる(伝われ)。
スクリーンショット 2021-01-21 10.35.21.png
中身が少なく下が空いてしまうときは、ContentViewとScrollViewの背景色をあわせて空いてないように見せるしかないのかしら?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SwiftUI] SceneDelegate を消して ApplicationDelegate だけにする

これはなに

iOS 13 環境で ReplayKit & UIScene の相性が悪いため、泣く泣く UISceneDelegate を消して UIApplicationDelegate だけにしたときのメモです。

ReplayKit & UIScene: Assertion when calling RPSystemBroadcastPickerView · Issue #438 · twilio/video-quickstart-ios

If you must use RPSystemBroadcastPickerView then you should consider not using UIScene, and going back to UIApplicationDelegate.

やったこと

SceneDelegate をバッサリ削除

Info.plist から UIApplicationSceneManifest をバッサリ削除します。

image.png

その後、SceneDelegate.swift も不要になるので削除します。(SceneDelegate でやっていたことがある場合は AppDelegate に移行する必要がある)

AppDelegate から UIHostingController経由で SwiftUI の View を開く

AppDelegate.swift を次のように書き換えます。

import UIKit
import SwiftUI

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow(frame: UIScreen.main.bounds)
        window!.rootViewController = UIHostingController(rootView: ContentView)
        window!.makeKeyAndVisible()

        return true
    }
}

このとき、func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
のような UISceneSession Lifecycle が残っているとうまくアプリが動かないので注意です。

これで一応アプリが起動するようになりますが、動作を保証するものではありません。あくまで自己責任で!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む