20200216のSwiftに関する記事は11件です。

Human Interface GuidelineにおけるModalのUIBarButtonItemの規則

iOS13になってから標準でmodalがiOSアプリ内でも使えるようになり、色々なアプリでmodalをみるようになったと思います。今回は凄いニッチな話ですが、UINavigationControllerで表示したmodalのUIBarButtonItemの命名規則についてまとめていきます。

想定する読者

  • アプリ開発をしているUIデザイナー
  • iOSエンジニア
  • Human Interface Guidelinesを勉強したい人

この記事のゴール

この記事を読んだ人・書いた僕自身が今後理由なくしてmodalのUIBarButtonItemの実装でHIGに従ってないアプリを作成しないこと

そもそもmodalとは

Human Interface Guidelinesによると

Modality is a design technique that presents content in a temporary mode that’s separate from the user's previous current context and requires an explicit action to exit. Presenting content modally can:

Help people focus on a self-contained task or set of closely related options
Ensure that people receive and, if necessary, act on critical information

日本語訳すると

modalとはユーザーにとって、前のコンテキストとは別の一時的な状態でコンテンツを提示するデザイン手法であり、終了するには明示的なアクションが必要なものである。モーダルは

  • 自己完結型のタスクまたは強い関連のある一連のタスクにユーザーが集中できるようにする
  • 人々が重要な情報を受け取り、必要に応じて行動する

ものである。

つまり、簡潔に言うとモーダルは

  • タスクを行うためのもの
  • タスクを完了するか、放棄することで閉じるもの

であることがわかります。

サンプル画像

いくつかAppleが出しているiOSアプリのnavigationBarのスクショを掲載します。
ファイル名
ファイル名
ファイル名
ファイル名
ファイル名
ファイル名

サンプル画像を見てわかること

キャンセルボタンと完了を表すボタンのみ

どの画面にもキャンセルまたは完了(を意味するボタン)がついています。

完了と同じ意味なら「完了」でなくていい

完了の他にも「追加」「投稿」など完了の意味を内包していれば完了を使わなくても良さそうなのが読み取れます。

アイコンは使っていない

僕は✖️のアイコンとかも使用すると勝手に思ってたんですが、Appleのアプリには存在しなさそうな雰囲気を感じます。
また、完了に似た「閉じる」のような文言も使ってないようです。

キャンセルはfontWeightがregular

タスクを完了することが目的なのでタスクを完了せず離脱する場合はregularみたいです。

完了を表すボタンはfontWeightがbold

逆に完了する場合はboldを使うのが推奨されるみたいです。

コードベースで見てみる

Documentでもこれまでの話と同じことがより簡潔に述べれられています。(この部分はmodalだけの話ではないです)
)スクリーンショット 2020-02-16 23.47.16.png

なのでこれを実行すると下の画像のようにdone(完了とか)では勝手にboldになるし、plain(キャンセルとか)ではregularになります。
スクリーンショット 2020-02-16 23.49.35.png

自分の作成してるiOSアプリや普段利用しているアプリがどうなってるのか見てみると面白いかもしれません。

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

【Swift5】UnitテストでArray(配列)のMatchableを作った時の備忘録

はじめに

Cuckooを使ったUnitテストで、
Array(配列)のMatchableを作成した際の備忘録を残しておきます。
なお、誤った記述等ございましたら、コメントをいただけますと幸いです。

実際に使用した用途・目的

Bluetoothクラスの実装で、
discoverCharacteristics(_:for:)メソッド
のテストコードを書く際に、このメソッドの引数である[CBUUID]?をMatchableに適合する必要がありました。

CBUUIDは、Bluetoothで使用する特殊な型です。
CBUUIDの使い所については【Swift5】Bluetoothクラス実装の備忘録にも記載しておりますので、宜しければご覧ください。

エラー内容

Argument type '[CBUUID]' does not conform to expected type 'OptionalMatchable'

実際にテストした内容

discoverCharacteristics(_:for:)メソッド
を呼んだ際に、引数のCBUUIDに適切な値がセットされて呼ばれているのかをテストします。

_ = XCTContext.runActivity(named: "Peripheral.discoverCharacteristics is called.", block: { _ in
    let uuidArray: [CBUUID] = [CBUUID(string: Const.Bluetooth.kUUID01),
                               CBUUID(string: Const.Bluetooth.kUUID02),
                               CBUUID(string: Const.Bluetooth.kUUID03)]
    verify(self.peripheral, times(1)).discoverCharacteristics(uuidArray, for: any())
})

解決方法・コード

エラーの解消には以下の2つのextensionが必要でした。

MatchableExtensions.swift
extension Array: OptionalMatchable where Element == CBUUID {
    public var optionalMatcher: ParameterMatcher<[CBUUID]?> {
        return equal(to: self)
    }
}

extension CBUUID: OptionalMatchable {
    public var optionalMatcher: ParameterMatcher<CBUUID?> {
        return equal(to: self)
    }
}

今回は、CBUUID?の配列に適合する必要があったので、Arrayのextensionと、CBUUIDのextensionの2つが必要でした。

まとめ

Cuckooを使っているからといって特殊な対応が必要というわけではなく、
Arrayのextensionが作れれば問題なし!といった感じでした。

応用も、基礎知識を持っていれば対応できるのですね・・・!

参考

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

【Swift5】超初心者でもできるCocoaPodsのインストール、ライブラリのインストールの仕方

はじめに

半年前くらいにSwiftを触りはじめました。
最初の頃は全くわからず、ネットで調べながらやっても、CocoaPodsの導入すらできませんでした。
ネットでCocoaPodsの導入に関する記事はたくさんあり、わかりやすく解説されているのですが、
超初心者には、この説明でもわかりませんでした。

ということで、僕みたいな超初心者でもCocoaPodsでライブラリをインストールできるよう、画像付きで解説しました。

CocoaPodsの導入の仕方

【CocoaPods】超初心者向け!CocoaPodsのインストールの仕方と使い方

スクリーンショット 2020-02-16 21.31.03.png

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

UITextViewにPlaceHolderを折り返し有りで追加してみる

概要

UITextViewにプレースホルダーを追加してみました。
かつ、プレースホルダーには折り返しをしてくれるようにしてみました。

はじめに

UITextViewにプレースホルダーを追加するだけのやり方であれば、
パイセン等が分かりやすくて素晴らしい記事を公開されてるので、そちらを参考にしてみてください。

UITextViewにプレースホルダーを設定できるようにする(Swift4)
[Swift 4.2] UITextViewにプレースホルダーを追加する[iOS 12]

私自身も大変参考にさせて頂きました?‍♂️
ありがとうございます?‍♂️?‍♂️?‍♂️
というか、ほぼほぼベースはパクらせてもらっ

こんなの

まずはじめに、単純にプレースホルダーをUILabelで追加しただけであれば、
こんな感じに折り返しが効かずに見切れてしまっています。

※『UITextViewのPlaceHolderをここに表示してます。』という文言を設定
Simulator Screen Shot - iPhone 8 - 2020-02-16 at 20.23.00.png

それがこんな感じに!
Simulator Screen Shot - iPhone 8 - 2020-02-16 at 20.22.42.png

もちろんxib上でも折り返して表示してくれます
スクリーンショット 2020-02-16 19.42.06.png

ソースコード

プレースホルダーの表示用として、UILabelをaddSubViewしており、
その際に、superViewであるUITextViewに対してAutolayoutの設定を入れることにより、折り返して表示してくれるようになってます。

PlaceHolderTextView.swift
import UIKit

@IBDesignable
class PlaceHolderTextView: UITextView {

    private let placeHolderLabelOriginPosition = CGPoint(x: 5.0, y: 8.0)
    lazy private var placeHolderLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.font = font
        label.backgroundColor = .clear
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)

        let labelWidth = frame.width - placeHolderLabelOriginPosition.x * 2
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor, constant: placeHolderLabelOriginPosition.y),
            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: placeHolderLabelOriginPosition.x),
            label.widthAnchor.constraint(equalToConstant: labelWidth)
        ])
        return label
    }()

    @IBInspectable var placeHolder: String = "" {
        didSet { reloadView() }
    }
    @IBInspectable var placeHolderColor: UIColor = .lightGray {
        didSet { reloadView() }
    }

    override var text: String! {
        didSet { changeVisiblePlaceHolder() }
    }


    override func awakeFromNib() {
        super.awakeFromNib()
        changeVisiblePlaceHolder()
        NotificationCenter.default.addObserver(self, selector: #selector(textChanged), name: UITextView.textDidChangeNotification, object: nil)
    }

}

extension PlaceHolderTextView {

    private func reloadView() {
        placeHolderLabel.text = placeHolder
        placeHolderLabel.textColor = placeHolderColor
        changeVisiblePlaceHolder()
    }

    private func changeVisiblePlaceHolder() {
        placeHolderLabel.isHidden = placeHolder.isEmpty || !text.isEmpty ? true : false
    }

    @objc private func textChanged(notification: NSNotification?) {
        changeVisiblePlaceHolder()
    }

}

おわりに

そもそも、プレースホルダーに折り返しが必要になるほどの長文を書くシチュエーションに遭遇するのでしょうか・・・?

追記

@am10 さんからのご指摘&ご提案から、コードベースでテキスト代入時にもプレースホルダーの表示切替が可能になりました!

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

Git リポジトリ検索APIをステップ・バイ・ステップでリファクタリング

はじめに

検証コードを書く時にGithub APIを使うことが多いですが、APIの種類を増やすのにコピー&ペーストして作成してしまっています。

”コピー&ペーストで重複したコードを増やさないようにしたい”、また、”データ通信ではなくJsonからデータ取得するテストコードと共存させたい” という動機からリファクタリングをしていきます。

今回リファクタリングするにあたっては、
【iOSDC2019 補足資料】具体的なコードから始めよ ~今の問題を解決し、ジェネリックなコードを見出す を参考にさせてもらっています。
いつも、とても勉強になる投稿本当にありがとうございます。

まずリファクタリング前のオリジナルのコードを見てみましょう。

リポジトリ検索のオリジナルコード

import Combine
import Foundation

protocol RepoService {
    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error>
}

class RepoServiceImpl: RepoService {
    private let session: URLSession
    private let decoder: JSONDecoder

    init(session: URLSession = .shared, decoder: JSONDecoder = .init()) {
        self.session = session
        self.decoder = decoder
    }

    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error> {
        guard
            var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
        else { preconditionFailure("Can't create url components...") }

        urlComponents.queryItems = [
            URLQueryItem(name: "q", value: query)
        ]

        guard
            let url = urlComponents.url
        else { preconditionFailure("Can't create url from url components...") }

        return session
            .dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: RepoResponse.self, decoder: decoder)
            .map { $0.items }
            .eraseToAnyPublisher()
    }
}

struct RepoResponse: Decodable {
   let items: [Repo]
}

Combineフレームワークを使っているので、スッキリ書けています。
このAPI一本だけなら、このように書くので良いと思います。

ただ、ユーザー検索APIを追加する時、上のコードをコピーして、以下の3箇所を変更することになり、共通点がとても多いことに気づきます。

  • エンドポイント変更
 var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
  
 var urlComponents = URLComponents(string: "https://api.github.com/search/users")
  • エンティティの型の変更
 -> AnyPublisher<[Repo], Error> {
   
 -> AnyPublisher<[User], Error> {
  • デコーダーの型の変更
.decode(type: RepoResponse.self, decoder: decoder)
   
.decode(type: UserResponse.self, decoder: decoder)

また、テストコードを書く場合にどうしたら良いのかも検討したいと思います。

コードの改善

共通部分の洗い出し&方針

  • APIのURLは、 /repositories /users のエンドポイント以外は完全一致しているので、エンドポイントを切り出せば良さそう。
  • Decode時に指定している型が RepoResponseUserResponse と異なりますが、Genericsを使えば解決できそう。
  • 戻りの型が -> AnyPublisher<[Repo], Error> となっていますが、これもGenericsを使えば解決できそう。

オフィシャルページを確認してみます。今回使用するこの2つはSearch機能に属するAPIです。パラメータは

  • q
  • sort
  • order

と共通です。

APIのResponseも itemsの外側は完全に一致しています。

{
  "total_count": 1,
  "incomplete_results": false,
  "items": [
    {
       // ...
    }
   ]
}

尚、Repositories のAPIとの共通化は検討しないものとします。

共通部分をAPIClientとして作成

APIのレスポンスを格納している、RepoResponseitems のみをプロパティに持つStructです。

struct RepoResponse: Decodable {
   let items: [Repo]
}

汎用化するにあたり、Repoassociatedtype で表現すれば解決できそうです。
/repositories のエンドポイントを apiBase として定義します。

この2点を加味すると以下のようなProtocolが作れます。

protocol Fetchable: Decodable {
    associatedtype Response
    static var apiBase: String { get }
    var items: [Response] { get set }
}

Fetchableに準拠させて、RepoResponseは以下のように書きかえられます。
今回は、型推論が効くのでtypealiasを指定を省略しています。

struct RepoResponse: Fetchable {
    var items: [Repo]
    static var apiBase: String { "/repositories" }
}

次に、RepoServiceImplUserServiceImpl で共通部分を抽出すると以下のようになります。

final class APIClient {
    let baseURL = "https://api.github.com/search"
    let session = URLSession.shared

    func fetch<Model>(_: Model.Type, query: String) -> AnyPublisher<[Model.Response], Error> where Model: Fetchable {
        guard var urlComponents = URLComponents(string: "\(baseURL)\(Model.apiBase)") else {
            preconditionFailure("Can't create url components...")
        }
        urlComponents.queryItems = [
            URLQueryItem(name: "q", value: query)
        ]

        guard let url = urlComponents.url else { preconditionFailure("Cant't create url from url components...") }

        return session.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: Model.self, decoder: JSONDecoder())
            .map { $0.items }
            .eraseToAnyPublisher()
    }
}

Genericsの Model は、where Model: Fetchable により、Fetchableに準拠させています。

これによりModelにapiBaseを定義していることになるので、APIのURLをbaseURL + apiBaseで表現することができるようになります。

戻り値は -> AnyPublisher<[Model.Response] となります。

decoderは .decode(type: Model.self, decoder: JSONDecoder()) となります(型を指定する必要があるので、 Model.selfとしています)

func fetch<Model>(_: Model.Type, ...)
このModel.Typeは関数の内部で使用されていませんが何なのでしょうか。

呼び出し側で、

apiClient.fetch(RepoResponse.self, ...)

とすることで Model == RepoResponse が成り立ちます。
つまり、Genericsの型を指定するためのパラメータであることがわかります。
Modelではなく、Model.Typeとしているのは、インスタンスではなく型を受け取るためです。

APIClient に共通部分の処理を移管できましたので、 RepoServiceImpl は以下のように書けます。大分スッキリしました!

final class RepoServiceImpl: RepoService {
    private let apiClient: APIClient
    init() {
        apiClient = APIClient()
    }

    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error> {
        apiClient.fetch(RepoResponse.self, query: query)
    }
}

テスト可能なコードに書き換え

上のコードは、URLSessionを使う前提の作りとなっているため、API通信が必要となってしまっています。

JSONを読み込んでその結果を返却するようなテスト用のMockコードを作成するには、APIClientが使えないので、 RepoServiceImpl とは独立した以下のようなClassを作成する必要があります。

final class RepoServiceMock: RepoService {
    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error> {
        // load data from json
        let data = repoData.items 
        if data.isEmpty {
            let error = FetchError.parsing(description: "Couldn't load data")
            return Fail(error: error).eraseToAnyPublisher()
        } else {
            return Future<[Repo], Error> { promise in
                promise(.success(data))
            }.eraseToAnyPublisher()
        }
    }
}

let repoData: RepoResponse = load("repositories.json")

func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }

    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }

    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

APIClient内部でURLSessionが定義されていなければ、この RepoServiceMock は RepoServiceImpl と共通化できそうです。

では、URLSessionを使わないように書き換えるにあたり、どの部分まで抽出できるか考えます。

        return session.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: Model.self, decoder: JSONDecoder())
            .map { $0.items }
            .eraseToAnyPublisher()

Decode部分はGenericsで型指定されているので、

return session.dataTaskPublisher(for: url)
            .map { $0.data }

その前の部分を切り出せれば良さそうです。

dataTaskPublisherについて調査

dataTaskPublisher が何か調べていきます。

このdataTaskPublisherはURLSessionの中にStructとして定義されているため、URLSessionありきのものであることがわかります。

extension URLSession {
 ...
    public func dataTaskPublisher(for url: URL) -> URLSession.DataTaskPublisher

    public struct DataTaskPublisher : Publisher {

        /// The kind of values published by this publisher.
        public typealias Output = (data: Data, response: URLResponse)

        /// The kind of errors this publisher might publish.
        ///
        /// Use `Never` if this `Publisher` does not publish errors.
        public typealias Failure = URLError

        public let request: URLRequest

        public let session: URLSession

        public init(request: URLRequest, session: URLSession)

        /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
        ///
        /// - SeeAlso: `subscribe(_:)`
        /// - Parameters:
        ///     - subscriber: The subscriber to attach to this `Publisher`.
        ///                   once attached it can begin to receive values.
        public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == URLSession.DataTaskPublisher.Failure, S.Input == URLSession.DataTaskPublisher.Output
    }
}

map { $0.data } はどんな返却値なの?

struct DataTaskPublisher : Publisher と定義されているので、
mapはPublisherで定義されているものとなります。

extension Publisher {

    /// Transforms all elements from the upstream publisher with a provided closure.
    ///
    /// - Parameter transform: A closure that takes one element as its parameter and returns a new element.
    /// - Returns: A publisher that uses the provided closure to map elements from the upstream publisher to new elements that it then publishes.
    public func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>

}

戻り値の型が、 Publishers.Map<Self, T> となっていますが、

Tはtransformで変換した戻り値の型であることがわかります。今回はData型に変換しているので、 T == Dataとなります。

Selfは何でしょうか?これは、自分自身 つまり、 URLSession.DataTaskPublisher となります。

つまり、URLSessionの一部の DataTaskPublisher を返却するPublisherなのでURLSessionから独立できません。

ほしいのはData型じゃないの?

今回リファクタリングするにこの発想に至るのが時間がかかりました。このままの形ではデータはURLSessionから独立できないので、データの型を AnyPublisher<Data, URLError> に変換してしまえば良いのではないでしょうか。

Transportの定義

AnyPublisher<Data, URLError> を戻り値の関数を持つ、Transportプロトコルを定義します。

AnyPublisher に変換するため、 .eraseToAnyPublisher() を追加します。

protocol Transport {
    func fetch(for url: URL) -> AnyPublisher<Data, URLError>
}

extension URLSession: Transport {
    func fetch(for url: URL) -> AnyPublisher<Data, URLError> {
        dataTaskPublisher(for: url).map { $0.data }.eraseToAnyPublisher()
    }
}

良さそうですね。 Retroactive Modelingも活用して、URLSessionにfetch関数を追加しています。これで、APIClientからURLSessionを排除できそうです。

APIClientは以下のように書き換えられます。

コンストラクタ時に、URLSession.shared = Transportとしているので今まで通り機能します。
リファクタリングする際にパラメータは q 以外に、 sortorder も指定できるように変更しました。

final class APIClient {
    let baseURL = "https://api.github.com/search"
    let transport: Transport

    init(transport: Transport = URLSession.shared) { self.transport = transport }

    func fetch<Model>(_: Model.Type, queries: [URLQueryItem]) -> AnyPublisher<[Model.Response], Error> where Model: Fetchable {
        guard var urlComponents = URLComponents(string: "\(baseURL)\(Model.apiBase)") else {
            preconditionFailure("Can't create url components...")
        }
        if !queries.isEmpty {
            urlComponents.queryItems = queries
        }
        guard let url = urlComponents.url else { preconditionFailure("Cant't create url from url components...") }

        return transport.fetch(for: url)
            .decode(type: Model.self, decoder: JSONDecoder())
            .map { $0.items }
            .eraseToAnyPublisher()
    }
}

コンストラクタとして、URLSession.sharedを受け取りますが、fetch関数の中にはURLSessionの記述はなくなりました。

利用する側は以下のようになります。

final class RepoServiceImpl: RepoService {
    private let apiClient: APIClient
    init(transport: Transport = URLSession.shared) {
        apiClient = APIClient(transport: transport)
    }

    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error> {
        apiClient.fetch(RepoResponse.self, queries: [URLQueryItem(name: "q", value: query)])
    }
}

テスト用のコードを作成

Jsonデータを読み込んだテストコードは、Transportに準拠させれば良いので、以下のようにシンプルに書けます。これでテスト用のコードでもAPIClientを使用するようにできました。

APIリクエストしてData型を取得、Jsonファイルを読み込んでData型を取得の部分が違うだけで後は処理が共通になりました。

final class TestRepoTransport: Transport {
    func fetch(for url: URL) -> AnyPublisher<Data, URLError> {
        let data = loadRawData("repositories.json")
        return Future<Data, URLError> { callback in
            callback(.success(data))
        }
        .eraseToAnyPublisher()
    }
}


func loadRawData(_ filename: String) -> Data {
    let data: Data
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    return data
}

RepoServiceImplのイニシャライズ時に、
API通信させたい場合は、RepoServiceImpl()
Jsonからデータを取得したい場合は、 RepoServiceImpl(transport: TestRepoTransport()) とするだけで差し替え可能になりました。

ソース全文

  • RepoService
import Combine
import Foundation

protocol RepoService {
    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error>
}

final class RepoServiceImpl: RepoService {
    private let apiClient: APIClient
    init(transport: Transport = URLSession.shared) {
        apiClient = APIClient(transport: transport)
    }

    func searchPublisher(matching query: String) -> AnyPublisher<[Repo], Error> {
        apiClient.fetch(RepoResponse.self, queries: [URLQueryItem(name: "q", value: query)])
    }
}

// MARK: - Test

final class TestRepoTransport: Transport {
    func fetch(for url: URL) -> AnyPublisher<Data, URLError> {
        let data = loadRawData("repositories.json")
        return Future<Data, URLError> { callback in
            callback(.success(data))
        }
        .eraseToAnyPublisher()
    }
}
  • APIClient
import Combine
import Foundation

final class APIClient {
    let baseURL = "https://api.github.com/search"
    let transport: Transport

    init(transport: Transport = URLSession.shared) { self.transport = transport }

    func fetch<Model>(_: Model.Type, queries: [URLQueryItem]) -> AnyPublisher<[Model.Response], Error> where Model: Fetchable {
        guard var urlComponents = URLComponents(string: "\(baseURL)\(Model.apiBase)") else {
            preconditionFailure("Can't create url components...")
        }
        if !queries.isEmpty {
            urlComponents.queryItems = queries
        }
        guard let url = urlComponents.url else { preconditionFailure("Cant't create url from url components...") }

        return transport.fetch(for: url)
            .decode(type: Model.self, decoder: JSONDecoder())
            .map { $0.items }
            .eraseToAnyPublisher()
    }
}

protocol Fetchable: Decodable {
    associatedtype Response
    static var apiBase: String { get }
    var items: [Response] { get set }
}

protocol Transport {
    func fetch(for url: URL) -> AnyPublisher<Data, URLError>
}

extension URLSession: Transport {
    func fetch(for url: URL) -> AnyPublisher<Data, URLError> {
        dataTaskPublisher(for: url).map { $0.data }.eraseToAnyPublisher()
    }
}
  • Repo
struct Repo: Decodable, Identifiable {
    var id: Int
    let owner: Owner
    let name: String
    let stargazersCount: Int
    let description: String?

    enum CodingKeys: String, CodingKey {
        case id
        case owner
        case name
        case stargazersCount = "stargazers_count"
        case description
    }

    struct Owner: Decodable {
        let avatar: URL

        enum CodingKeys: String, CodingKey {
            case avatar = "avatar_url"
        }
    }
}

// struct RepoResponse: Decodable {
//    let items: [Repo]
// }
struct RepoResponse: Fetchable {
    var items: [Repo]
    static var apiBase: String { "/repositories" }
}
  • LoadData
let repoData: RepoResponse = load("repositories.json")

func loadRawData(_ filename: String) -> Data {
    let data: Data
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    return data
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(at)PublishedがどのようにSwiftUIのViewを更新するのか

(at)PublishedがどのようにSwiftUIのViewを更新するのか

@Published属性がどのようにSwiftUIのViewを更新するのかについて書いておきます。

WWDC19の動画を見るとそもそも@Published属性がまだ存在していないのと、その当時はObservableObject型がBindableObject型であったり、@ObservedObject属性が@ObjectBinding属性だったりするのでややこしいためです。

私の結論としては@Publishedあると便利なんだけど、コードを書き換えてるときになぜこのプロパティに@Publishedにしたんだろう、という感覚になりやすい。おとなしく手動でやっといたら意図も読みやすくて混乱しなかったのにという気持ちになりやすいからですかね。

@ObservedObject属性として指定されたObservableObjectはSwiftUIにsubscribeされている

  • Viewは@ObservedObject指定したObservableObjectobjectWillChangeをsubscribeしている
    • subscribeし発火されるとViewを更新するようにしている
    • ObservableObjectobjectWillChangeを使い発火させればいい
  • @PublishedはプロパティのwillSetでobjectWillChangeを使い発火させるのを暗黙的に行っている

objectwillchange_subscribe_from_swiftui.png

objectWillChangeとは何か

objectWillChangeはCombineで定義されているObservableObjectを見るとわかります。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {

    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher  // 制約の追加 Swift4.1(SE-0157) 
       = ObservableObjectPublisher  // デフォルトで利用する型
         where Self.ObjectWillChangePublisher.Failure == Never // 条件

    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}
  • プロパティobjectWillChangeSelf.ObjectWillChangePublisherを返す
  • associatedtypeのObjectWillChangePublisher
    • Publisherプロトコルを制約として追加している
    • 言い換えるとObservableObjectはパブリッシャーを保持している
    • デフォルトではObservableObjectPublisherクラスを使う
      • さらにエラーは流さない
      • おそらくカスタマイズしたPublisherも使える

ObservableObjectPublisherとは何か

Combineで用意されたObservableObjectPublisherクラスを確認してみます。

/// The default publisher of an `ObservableObject`.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final public class ObservableObjectPublisher : Publisher {

    /// The kind of values published by this publisher.
    public typealias Output = Void

    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never

    public init()

    /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
    ///
    /// - SeeAlso: `subscribe(_:)`
    /// - Parameters:
    ///     - subscriber: The subscriber to attach to this `Publisher`.
    ///                   once attached it can begin to receive values.
    final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output

    final public func send()
}
  • OutputはVoidなのでイベントは (Void) -> Voidのクロージャ
    • .sink { (Void) -> Void in ... }
      • SwiftUIのViewとしては更新されたらbodyが実行される
        • 使用されるオブジェクトのプロパティは全て読みだされるので
          • イベント時のOutputがVoidでも特に気にしない
  • どのような場合にカスタマイズする?
    • Outputを変更したい場合がある?
      • 思いつかないな...

必ずしも@Publishedを使う必要はない

@Publishedを使いたいのは次の優先度のとき

  • ViewがBindingなオブジェクトを取り出してバインディングしたい(読み込みと書き込み)
  • ObservableObjectの中でfilterflatMapしたい
  • objectWillChange.send()を自動でやりたい

RxSwiftのようなUIからストリームを作れる仕組みがないために、Bindingでの値のやり取りが楽なので@Publishedを使いたい。本当はそれ一択かもしれない。

SwiftUIガイドブック

BOOTHで電子書籍として販売しています。

https://booth.pm/ja/items/1829015

読者のターゲットは次のような人を想定しています

  • SwiftUIのレンダリングシステムが自動で更新されるタイミングを知らない
    • 更新のきっかけになる$ObservedObject@Publishedにもやもやしている
    • SwiftUIのViewが自動で更新されるタイミングやその範囲にもやもやしている
  • @State@Binding@ObservedObjectの違いがよくわからない
    • @Environment@EnvironmentObjectの違いもまだ知らない
  • $オペレータを雰囲気で使って雰囲気で修正している
    • コンパイルエラーになるたびに修正している
    • propertyWrapperとdynamicMemberLookupを理解して使っていない

SwiftUIでpropertyWrapperとdynamicMemberLookupがあやふやだなーという気持ちになったままコードを書くことも多いのではないかと思ってます。

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

【Swift5】カラー指定する方法のチートシート永久保存版

この記事を書いた理由

Swiftでのカラー指定の方法は、たくさんあります。
毎回毎回、やりたいカラー指定のやり方を調べるのがめんどくさかったため、自分でカラー指定のチートシートを作成しました。

環境

ツール version
Swift version 5.1.3
Xcode Version 11.3 (11C29)

チートシート

目次

  1. storyboardでカラー指定する方法
  2. UIColorのプロパティでカラー指定する方法
  3. UIColorでRGB(CGFloat型)でカラー指定する方法
  4. UIColorを無理やりRGBでカラー指定する方法
  5. UIColorを16進数のカラーコードでカラー指定する方法
  6. UIColorをRGBでカラー指定する方法
  7. カラーリテラルを使ってカラー指定する方法
  8. Assets.xcassetsで色を管理し、カラー指定する方法 スクリーンショット 2020-02-16 13.30.02.png

他のチートシート

画面遷移のチートシート

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

SwiftUI VStackこういうことだったのか劇場

背景

SwiftUIにて画面作成するときにほぼ間違いなくお世話になる、VStack、HStackについて、その公式ドキュメントのリファレンスと実装に差異があり戸惑いました。そのときの疑問とそれをどのように整理して解消したのかを記録したものです。

対象とする読者

Swift初心者
SwiftUI初心者

環境

私がこの記事を書いている際に利用しているのは次の環境です。
Xcode 11.3.1
Swift 5.1

はじめに

VStack、HStackは、SwiftUIで画面レイアウトを作るときにほぼ間違いなくお世話になるもので、ビュー(ここでいうビューは、ボタンやテキスト等のUIコントロール部品や、それらをグループ化した子ビューなど広義でのUIコントロール全般を指すものとご理解ください)をレイアウトとして配置するためのコンテナーとして利用され、それ自体もビューとなります。

ビューを縦(垂直)方向に並べるときに利用するのが、VStack
ビューを横(水平)方向に並べるときに利用するのが、HStack
ビューを奥行き方向に並べるときに利用するのが、ZStack

入れ子でこれらを使い分けることで、様々なレイアウトにカスタマイズして画面を構成していきます。

今回は、このうちのVStackを取り上げます。

VStack

イニシャライザの呼び出し方

イニシャライザとは、インスタンスの初期化用メソッドのこと。

公式ドキュメントの記載は次のとおりです。

公式ドキュメント(VStack)
https://developer.apple.com/documentation/swiftui/vstack

// Creating a Stack
init (alignment: HorizontalAlignment, spacing: CGFloat?, @ViewBuilder content: () -> Content)

VStackイニシャライザのパラメータは3つあります。

  • alignment 水平方向の配置位置をHorizontalAlignment構造体のタイププロパティで指定。 .leadeing(左寄せ), .center(中央寄せ), .trailing(右寄せ)等の指定が可能。デフォルトは.center
  • spacing 子ビュー同士の間隔を調整するための余白を指定します。デフォルトはnilでシステムデフォルトの余白が付与されます。
  • content このスタックに並べるビュー(子ビュー、ボタン、テキストなど)をまとめたものを返すクロージャを指定します。

デフォルトのあるパラメータは省略可能です。一番最後のcontentだけは必ず指定が必要です。

実際の使い方

VStack(alignment: .leading, spacing: 10) {
    Text("Hello World!")
    Text("Here We Go!")
}

実行結果
vstack_result1.png

もし私のようなSwift初心者は、ここで少し混乱します。

  • 疑問1 イニシャライザの定義だと引数が3つあるけど、そのうちの最後のcontentが欠落している?
  • 疑問2 その代わりに、{}内にVStack内に表示させたいビュー(Text2つ)を記述している?
  • 疑問3 そもそも{}内にビュー(Text2つ)を改行して列挙してだけのようだけど、こういう書き方していいの?

では、これらの疑問を見ていきます。

疑問1と疑問2について

swiftでは「トレーリングクロージャー式」という記述をすることが可能です。
トレーリングは後方を指し、クロージャー式は{}で囲んだ式のこと。

トレーリングクロージャーというのは、以下のようなSwift独自の構文です。

関数の最後の引数としてクロージャー式を渡す必要があり、クロージャー式が長い場合に、それを関数の引数として記載せずに、関数呼び出しの括弧の後(末尾)にクロージャー式{ }として記述できる

つまり、さきほどの実装例はトレーリングクロージャー式で記述されていたということです。それを利用しなかった場合は次のよう書き換えることができます。

// トレーリングクロージャー式を使わないで書くと・・・
VStack(alignment: .leading, spacing: 10, 
    content: {
      Text("Hello World!")
      Text("Here We Go!")
    }
)

この例では、たしかにイニシャライザの宣言になんとなく寄ってきました。クロージャーの中身が数行なので、読みにくいということもありません。

ですが、クロージャーに記述する行数が増えれば増えるほど、読みずらくなるのは容易に想像できますので、基本はトレーリングクロージャー式を利用して、重たい記述は引数とは別に記述すべきなのですね。とても合理的な考え方だと思います。

疑問3について

疑問1,疑問2 についてはコードの書き方として便利な式が用意されていることを知りました。しかし、疑問3の「そもそも{}内にビュー(Text2つ)を改行して列挙してだけのようだけど、こういう書き方していいの?」については、まだ解決していません。

ここで、改めてVStackのイニシャライザを見てみると、content引数には、@ViewBuilder属性が付与されていることがわかります。

この属性はSwift5.1の新機能である「ファンクションビルダー」が利用されています。
この属性がついたクロージャー内の各行は、暗黙的に1行が1引数となって、ViewBuilderbuildBlockメソッドに渡されます(暗黙的に内部で変換されます)。

つまり、クロージャー内にTextを2行記述しましたが、実際は内部では以下のように2つの引数を受け取るbuildBlockメソッドに渡され、View型のインスタンスを返却してもらっているのです。

// ViewBuilderへの変換がされた後の実装はこんな感じ
//(こちらの実装でも、もちろん表示されます)
VStack(alignment: .leading, spacing: 10, 
    content: ViewBuilder.buildBlock(
              Text("Hello World!"),
              Text("Here We Go!") 
)

ここまで記述すると、ようやくイニシャライザの定義と完全に理論的な一致が確認でき、安心できます。

なお、ViewBuilderでは、VStack、HStackなどクロージャー内部に最大10行のViewまでを引数として置き換えてくれるbuildBlockメソッドが事前に用意されています。VStackに10行分のTextを記述しても正常に画面表示できますが、11行以上記述するとビルドエラーが発生します。時間がある方はぜひ実際に試してみてください。

さいごに

新しい関数(イニシャライザ)を見て、リファレンス(公式ドキュメント)を参照するのは開発者として基本的な行動です。ですが、実際のコードとリファレンスに乖離があると、混乱したり正しい理解の妨げになります。

私も今回のこの記事を自分で整理してアウトプットすることで、頭の中を整理できました。私のようなSwift初心者の方に、この記事が少しでも役立てば幸いです。

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

Swift 基礎知識 1

はじめに

現在、私はRubyを中心に学んでおります。
学習を進める中、他言語にも興味が沸いた為、Swiftに関して用語等を整理していたいと思いました。
もうすでにご存知の方、省略の仕方等ご存知でしたら、ご教授願います。

Swiftとは

  • ネイティブアプリを開発するためのプログラミング言語

    • ネイティブアプリ : あるプラットフォームで直接動作するアプリのこと(スマホとかですね)
  • 対象OS

    • MacOSとiOS
    • 上述の通り、iPhoneやiPadなどの端末で使えるアプリ、もしくはMacパソコン向けのアプリを開発することができる。

今までのネイティブアプリ開発は?

  • これまではObjective-Cが使われていた。(現在も使える様です)
  • RubyやPHPなど最新の設計思想を盛り込んでいて、スクリプト言語のように簡単にアプリを開発することができる。

特徴

  • SwiftはXcodeを使って開発をする。
    • XcodeはApple IDがあれば無料でダウンロードすることができる。

勝手なイメージですが、敷居が高くなく、すぐに入り込めそうな感じがします。

さいごに

日々勉強中ですので、随時更新します。
皆様の復習にご活用頂けますと幸いです。

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

loadBodyTrackedAsync が使えない際の対処法

現象

WWDC 2019で紹介されていたARKit & RealityKitのサンプルコードを実装した際、loadBodyTrackedAsyncがEntity型のメンバーではないというエラーが表示された。

スクリーンショット 2020-02-16 8.27.29.png

対処法

ビルドの矛先をGeneric iOS Deviceもしくは実機に向ける。

スクリーンショット 2020-02-16 8.32.18.png

関連リンク

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

無料でPHP使ってWebAPIをつくってみた

はじめに

以前つくった文字列→塩基配列の相互変換ツールをつくってみた(アプリ版)のWeb版(PHP)がこれにより不要になってしまいました:scream:

せっかく作ったPHP版も何かに使えないかと思いAPI化してみました:clap:

*注意 httpです:see_no_evil:

成果物

ドキュメント

BluePrintでAPIドキュメントみたいなのつくってみましたので詳細はこれを見てください:bow:

http://adventam10.php.xdomain.jp/dna/api/

ソース

下記の dna-converter.php です。

https://github.com/adventam10/DNAConverter-web/tree/master/api

API化

PHPで簡単なWebAPIを実装してみるを参考に実装!!

こんな感じで実装して下記のように XFREE にアップ!!!

無料でできるPHPのWEBサイト公開(テスト用)

<?php
header('Content-Type: text/html; charset=UTF-8');

$json_string = file_get_contents('php://input');
$json = json_decode($json_string, true);
$resultCode = checkResultCode($json);
if($resultCode === 0) {
    $arr["resultCode"] = $resultCode;
    $arr["convertedText"] = convert($json["text"], $json["mode"]);
} else {
    $arr["resultCode"] = $resultCode;
}

print json_encode($arr, JSON_PRETTY_PRINT);

swift で確認してみる

let urlString = "http://adventam10.php.xdomain.jp/dna/api/dna-converter.php"
var request = URLRequest(url: URL(string: urlString)!)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = try? JSONEncoder().encode(Request(mode: 0, text: "あいうえお"))
let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
  let response = try? JSONDecoder().decode(Response.self, from: data!)
  print(response)
}
task.resume()

struct Request: Codable {
    let mode: Int
    let text: String
}

struct Response: Codable {
    let resultCode: Int
    let convertedText: String
}

ちゃんと以下のように取得できていました:clap:

Optional(Test.Response(resultCode: 0, convertedText: "GCAGCAATCAACGCAGCAATCATAGCAGCAATCATCGCAGCAATCACAGCAGCAATCACC"))

ハマったとこ

POSTMAN を使って確認していたのですが下記のようにリクエストをしていると mode が数値ではなく文字列になってしまいハマりました。

postman_1

その時のPHPの実装が下記

$resultCode = checkResultCode($_POST);
function checkResultCode($param) {
  if (isset($param["mode"])) {
    $mode = $param["mode"];
    if ($mode === 0 || $mode === 1) { // 文字列なのでここでアウト
      if (!empty($param['text'])) {
        $text = $param['text'];
        if ($mode === 0) {
          return 0;
        } else {
          if (isInvalidDNA($text)) {
            return 4;
          } else {
            return 0;
          }
        }
      } else {
        return 3;
      }
    } else {
      return 2;
    }
  } else {
    return 1;
  }
}

POSTMAN で下記のように JSON を設定すると数値が送れるらしいと聞いたので修正。

postman_2

が、しかし、PHP が $_POST だったので値を受け取れず:scream:

どうやら $_POST では JSON を受け取れない模様。

参考:JSON形式でPOSTされたデータの受信方法

下記のように PHP を修正。

$json_string = file_get_contents('php://input');
$json = json_decode($json_string, true); // 第2引数にtrueを設定しないとダメらしい
$resultCode = checkResultCode($json);
function checkResultCode($param) {
  if (isset($param["mode"])) {
    $mode = $param["mode"];
    if ($mode === 0 || $mode === 1) {
      if (!empty($param['text'])) {
        $text = $param['text'];
        if ($mode === 0) {
          return 0;
        } else {
          if (isInvalidDNA($text)) {
            return 4;
          } else {
            return 0;
          }
        }
      } else {
        return 3;
      }
    } else {
      return 2;
    }
  } else {
    return 1;
  }
}

無事数値を取得できました:clap:

ドキュメント作成

せっかくなんで API Blueprint でドキュメントを作成してみました。

参考:API BlueprintでWeb APIのドキュメントを生成する

ハマったとこ

node.js を https://nodejs.org/ja/ ここから取得して入れていたので npm install -g aglio がパーミッションエラーになってしまいました:scream:

下記参考に設定を行い無事インストールできました:clap:

npmでpermission deniedになった時の対処法[mac]

ドキュメントの書き方あんまわからなかったので下記参考にしながらそれっぽいものを書いてみました。

API Blueprint を使って Web API の仕様書を書こう

さいごに

とりあえずこれで PHP で作成したものも無駄にならずにすみました:tada:

これでどこからでもDNA変換ができます!!!

(swift の POST がライブラリ使わずにやるのやり方忘れてて地味に苦労しました:speak_no_evil:

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