20200820のSwiftに関する記事は12件です。

UITableviewをスクロールした際、Section headerを固定しない

自分用メモ

SwiftでUItableviewを使っている時、スクロールしたときにHeaderが画面に残るのをFloatにする。

コード

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let sectionHeaderHeight:CGFloat  = 40;
        if (scrollView.contentOffset.y <= sectionHeaderHeight && scrollView.contentOffset.y >= 0) {
            scrollView.contentInset = UIEdgeInsets(top: -scrollView.contentOffset.y, left: 0, bottom: 0, right: 0);
        } else if (scrollView.contentOffset.y >= sectionHeaderHeight) {
            scrollView.contentInset = UIEdgeInsets(top: -sectionHeaderHeight, left: 0, bottom: 0, right: 0);
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】NavigationView タイトルカスタマイズ方法

タイトルとアクション等の設定

NavigationView に属性をつけるのではなく中身につけることに注意。

NavigationView {
    VStack {
        Text("How to customize view")
    }
    .navigationBarTitle(Text("Hello"))
    .navigationBarItems(trailing: Button("Done", action: {
        // Action
        // 左に作るときは leading: にする
    }))
    // .inline .automatic .large
    .navigationBarTitleDisplayMode(.automatic)
    .navigationBarBackButtonHidden(true)
}

タイトルのフォント/背景色の変え方

appearance()はあまり使わない方がいいという意見もありますが、これがとりあえず一番やりやすいかと思います。
UIViewController の属性を引き継いでいるので、中身はUIFont UIColor で設定します。

struct MyView: View {
    init() {
         // 背景を透明にする
         UINavigationBarAppearance().configureWithTransparentBackground()

         // コンパクトなナビゲーションバーの時の背景色を変える
         UINavigationBar.appearance().barTintColor = .systemRed

         // コンパクトなナビゲーションバーのタイトル属性
         UINavigationBar.appearance().titleTextAttributes = [
             .font: UIFont.systemFont(ofSize: 10)
             .foregroundColor: .systemBlue!,
         ]
         UINavigationBar.appearance().largeTitleTextAttributes = [
             .font: UIFont.systemFont(ofSize: 20)
             .foregroundColor: .systemBlue!,
         ]
     }
     var body: some View {
          // Navigation View
     }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【今日からできる】SwiftUI/Combineのユニットテスト

こちらの記事は、YUMEMI.swift #9 ~テストと自動化~で発表した内容をもとに書いたものです。

サンプルプロジェクトはこちらにあります。

SwiftUIには公式のテスト用APIが用意されていない

UIKitなら例えば以下のようにテストを行うことができます。

let viewController = ViewController()

// viewDidLoadを呼ぶためのメソッド
viewController.loadViewIfNeeded()

// subviewsからボタンを検索
let button = viewController
    .view
    .subviews
    .first(where: { $0 is UIButton })

// 存在確認
XCTAssertNotNil(button)

// タップイベントを発火
button!.sendActions(for: .touchUpInside)

SwiftUIのViewがユニットテストできないなんて誰が言ったの?

検索してみると……

Who said we cannot unit test SwiftUI views?1

という記事がヒットしました。どうやらこの記事を書いた人がViewInspectorというテスト用のフレームワークを作成してくれているそうです。

現状では、SwiftUIのユニットテストができる唯一無二のフレームワークで、公式のフレームワークが出るまではスタンダードになっていきそうな予感がします。

ViewInspector

こんな感じのAPIが用意されています。

// ボタンタップを実行
let button = try view
    .inspect()
    .hStack()
    .button(1)
try button.tap()

// onAppearをシミュレート
let list = try view
    .inspect()
    .list()
try list[5]
    .view(RowItemView.self)
    .callOnAppear()

実際に使ってみる

サンプルのプロジェクトはよくある、GitHubのリポジトリを検索してくるやつです。

こんな感じのUIで、Listの下に"Search More Swift"というButtonが表示されています。これが表示されているかどうかをユニットテストで確認していきます。

準備:DIのためにprotocol経由で参照を持たせる

テストしやすいようにViewにprotocol経由でViewModelを持たせます。

// View
struct SearchRepositoriesView<ViewModel: ViewModelProtocol>: View {
    // ObservableObject protocolがassociatedtypeを持っているため
    // protocolのまま型指定できないので、Generic Typeを利用する
    @ObservedObject private var viewModel: ViewModel
    ...
}

// ViewModel
protocol ViewModelProtocol: ObservableObject {
    var lastQuery: String? { get }
    var state: ViewModelState { get }
    // Binding<Bool>として使うのでsetが必要
    var shouldShowErrorAlert: Bool { get set }
    var repositoryList: [GitHubRepo] { get }
    var moreSearchResultsExist: Bool { get }
    func send(event: SearchRepositoriesViewModelEvent)
}

ViewInspectorを使って要素を取得する

注意点として、「子要素を複数持つ可能性のある要素」から子要素を取得するときには、子要素(例えばVStack)に対応したメソッド(vStack)に、そのindex(親要素の中で何番目にあるか)を渡す必要があります。

テストコード
// 対象のViewにInspectable protocolを追加
extension SearchRepositoriesView: Inspectable {}
...

// ViewModelのスタブを作成
stubViewModel = SearchRepositoriesViewModelStub()

// 依存性注入
view = SearchRepositoriesView(viewModel: stubViewModel)

stubViewModel.state = .ready
stubViewModel.repositoryList = ...
stubViewModel.moreResultsExist = true

let text = try view!.inspect()
    .zStack()
    .vStack(0) // Zstackのindex:0の位置
    .list(1)   // Vstackのindex:1の位置
    .button(1) // Listのindex:1の位置
    .text()

XCTAssertEqual(try text.string(), "Search More ")
Viewの構造
ZStack {
    VStack { // index: 0
        SearchBar(...) // index: 0
        List { // index: 1
            ForEach(...) { ... } // index: 0
            if ... {
                Text("No results found...")
            } else if self.viewModel.moreResultsExist {
                Button(action: { ... }) { // index: 1
                    if self.isLoading {
                        ActivityIndicator()
                    } else {
                        (Text("Search More ") + // Target!!!
                            Text(self.lastQuery)
                                .font(.headline)
                                .bold()
                                .italic()
                            )
                    }
                }
            }
        }
    }
    if isLoading && isListEmpty {
        ActivityIndicatorWithRectangle()
    }
}

カスタムViewの存在確認

今度はTextではなく、カスタムViewが表示されていることを確認します。
先ほどの"Search More Swift" Buttonをタップすると、インディケーターが周りだします。

テストコード
stubViewModel.state = .loading
stubViewModel.repositoryList = ...
stubViewModel.moreResultsExist = true

let button = try view!.inspect()
    .zStack()
    .vStack(0) // Zstackのindex:0の位置
    .list(1)   // Vstackのindex:1の位置
    .button(1) // Listのindex:1の位置

// カスタムViewを取り出す
// viewメソッドにstructのselfを渡したあと、actualViewメソッドで
// ActivityIndicatorのインスタンスが取得できる
XCTAssertNoThrow(
  try button
      .view(ActivityIndicator.self)
      .actualView()
)

// (おまけ)Textが入っていないことも確認
XCTAssertThrowsError(try button.text())
ActivityIndicator
struct ActivityIndicator: UIViewRepresentable {
    func makeUIView(
        context: Context
    ) -> UIActivityIndicatorView {
        let indicator
            = UIActivityIndicatorView()
        indicator.startAnimating()
        return indicator
    }
    ...
}
Buttonの内部(再掲)
Button(action: { ... }) {
    if self.isLoading {
        ActivityIndicator() // Target!
    } else {
        (Text("Search More ") +
            Text(self.lastQuery)
                .font(.headline)
                .bold()
                .italic()
            )
    }
}

SwiftUIのユニットテスト → やりやすい

SwiftUIではViewが状態の関数であることが強制されるおかげで、何をテストすべきかがわかりやすいため、ユニットテストが書きやすいです。

Combineのユニットテスト

CombineのユーティリティフレームワークであるEntwineを使います。

テスト補助用のクラスとしてTestSchedulerが用意されているので、これを使ってViewModelのテストをやってみます。

ViewModel
protocol ViewModelProtocol: ObservableObject {
    // 最後に使ったクエリを取得できる
    var lastQuery: String? { get }
    // View側からイベント通知をするためのメソッド
    func send(event: SearchRepositoriesViewModelEvent)
    ...
}

class ViewModel: ViewModelProtocol {
    // サーバーとの通信を行うModel
    private let reposModel: GitHubReposModelProtocol

    // 最後に使ったクエリは@Publishedをつけて、SwiftUI Viewの監視対象
    @Published private(set) var lastQuery: String?
    ...
}

スケジューラーにイベントの通知をスケジューリングしていきます。

stubModel = GitHubReposModelStub()
viewModel = SearchRepositoriesViewModel(model: stubModel)

let scheduler = TestScheduler.init(initialClock: 0)
// t = 100に "Swift" という queryを発行
scheduler.schedule(after: 100) {
    self.viewModel.send(event: .didSearchButtonClicked(query: "Swift"))
}
// t = 200に "Python" という queryを発行
scheduler.schedule(after: 200) {
    self.viewModel.send(event: .didSearchButtonClicked(query: "Python"))
}

テスト用のsubscriberを作成して、テスト対象であるviewModel.$lastQueryにsubscribeします。

// テスト用のsubscriberを作成
let subscriber = scheduler.createTestableSubscriber(String?.self, Never.self)
viewModel.$lastQuery.subscribe(subscriber)

スケジューラーを起動すると、テスト用のsubscriberがイベントを受け取り、TestSequenceとして記録してくれます。

// スケジューラを起動
scheduler.resume()

let expected: TestSequence<String?, Never> = [
    (000, .subscription),
    (000, .input(nil)),
    (100, .input("Swift")),
    (200, .input("Python")),
]
// 受け取ったイベントと期待されるイベントを比較
XCTAssertEqual(subscriber.recordedOutput, expected)

おわりに

意外と簡単にユニットテストが書けたのではないでしょうか?
どんどんテストを書いていきましょう!

参考

UIViewControllerをXCTestでUnitテストする
Who said we cannot unit test SwiftUI views?
ViewInspector
Entwine


  1. 階層化されたSwiftUIのViewから、どうやって中身を取り出していくかという苦労が綴られていて面白いです。 

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

長押し続けている間繰り返し処理をするLongPressGestureRecognizer

UILongPressGestureRecognizer + Timerで

押し続けている間、連続で処理をするボタンをつくりたい。
たとえば、押している間ミサイルを連写するとか。
UILongPressGestureRecognizerの state.beganでタイマーのリピート処理を始め、 state.endedで Timer.invalidate( ) することで実現できます。

@objc func repeateWhilePressing(_ gestureRecognizer : UILongPressGestureRecognizer){
        switch gestureRecognizer.state {
            case .began:
        print("shot!")//短いタップでも反応するようにここにも処理を書いておきます。

        pressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (Timer) in
                       print("shot!")//押している間0.1秒間隔で繰り返し。
                   })
               case .ended:
                   pressTimer?.invalidate()
               default:
                   break
               }
    }

短いタップの場合も反応するように、Long Pressの最小プレス時間を小さく設定。

let longPress = UILongPressGestureRecognizer(target: self, action: #selector(repeateWhilePressing()))
longPress.minimumPressDuration = 0.01

押している間の結果

shot!
shot!
shot!
shot!
shot!


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

iOS14「SKAdNetwork」で必要な対応をまとめました。

概要

まず「SKAdNetwork」への対応ですが

実際のところ、導入している広告SDK側や計測SDK側で実装されるケースが多いとは思います。

しかし、各SDK側でどんな対応をしているのか?については分かっていた方が良いので
(例えばSDK同士の干渉問題が出たりした際など)

  • 広告主側
  • 広告表示側(パブリッシャー、媒体側)
  • 広告ネットワーク

以下にそれぞれの「SKAdNetwork」対応内容について整理しました。

最初に、SKAdNetworkとは

class SKAdNetwork : NSObject

「IDFAを使わない広告成果トラッキング」の為に、iOS14で更新されるクラスです。

Appleが提供する広告効果の確認方法となります。

これまでは「どの広告のどのキャンペーンか?」くらいだった粒度の情報が

「どのアプリからきたか?+ α」までわかるようになる予定です。

ただし、Apple公式ということで多くの制限があり

細かなユーザ追跡を基盤に価値を提供してきた計測プロバイダー(MMP)からは

「このクラスは役不足」「この仕組みで満足な計測を行うことは出来ない」との声も多い様です。

広告主側アプリでの対応

広告主側は、色々な他社アプリ内に広告を掲載している側のアプリです。

要するに、お金をかけて掲載した広告効果を計測したい側です。

Appleのドキュメントでは「Advertised app」と表現されています。

インストール(起動)成果の通知処理を実装する必要があります。

成果通知の実装

外部SDK内部で実行されているケースが主ではあると思いますが、

アプリ側では起動時に

class func registerAppForAdNetworkAttribution()

または

class func updateConversionValue(_ conversionValue: Int)

を呼び出す必要があります。

どちらでも成果を通知させることが出来ます。

前者のregisterAppForAdNetworkAttribution()は初回呼び出しのみが有効で

後者のupdateConversionValue(_:)では、

任意のコンバージョン値(6bit制限あり)を広告ネットワークへ成果通知と一緒にポストバックすることが出来ます。

このコンバージョン値の活用方法としては、こちらが参考になりました。

Announcing Flurry Conversion Value Analytics for... | Flurry Blog

Flurry SDKでは、このコンバージョン値を使って

ユーザを大まかにセグメント分けしたり、比較する方法に加え

主要なイベントをトラッキングするオプション機能を提供する様です。

例えば、ログインやアプリ課金などの特定イベント発生時に

updateConversionValue(_:)を呼び出して
(registerAppForAdNetworkAttributionでスタートした24hタイマーが切れる前に呼ぶ必要がある。2020/08/20 16:15追記)

コンバージョン値を広告ネットワークへポストバック

という少しイレギュラー?な方法みたいです。

何れにしても、広告ネットワークへの通知が、リアルタイムでは無い点には注意が必要です。

The first call to registerAppForAdNetworkAttribution() generates the notification if the device has attribution data for that app, and starts a 24-hour timer. Subsequent calls to this method have no effect. You may, however, call updateConversionValue(_:) to provide a conversion value and restart the timer.
The device sends the install notification to the ad network’s postback URL within 0-24 hours after the timer expires. Ad networks must verify the postback after receiving it. See Verifying an Install Validation Postback for more information.
Apple Documentationより

とあるので

メソッドトリガーでの24時間タイマースタート + そこから0〜24時間でポストバック

つまり、最短24時間、最長で48時間のタイムラグが発生する可能性があります。

これがもしリアルタイムだと、ユーザーをある程度紐付けする事が出来てしまうので、その対策だと思われます。

少し話が逸れましたが、広告主側アプリとしては

  • 成果通知処理の実装(自前実装orSDK)
  • コンバージョン値の活用(オプション)

が対応ポイントです。

広告表示側アプリでの対応

アプリ内で広告を掲載しているアプリでの対応です。

メディア系、カジュアルゲーム系などのいわゆる広告モデルで収益を上げているアプリです。

Appleのドキュメントでは「Source App」という表現されています。

こちら側のアプリでは、掲載する全ての広告ネットワークIDを登録する必要があります。

Info.plistに広告ネットワークのIDを登録

アプリ内で表示する全ての広告ネットワークIDをInfo.plistに登録します。

このIDは、広告ネットワーク側へ問合せて事前に用意する必要があります。

広告ネットワークIDの登録方法は、Info.plistのキーに

SKAdNetworkItems

を入力。
以下の形式で、全ての広告ネットワークIDを登録していきます。

<array>
    <dict>
        <key>SKAdNetworkIdentifier</key>
        <string>example100.skadnetwork</string>
    </dict>
    <dict>   
         <key>SKAdNetworkIdentifier</key>
         <string>example200.skadnetwork</string>
    </dict>
</array>

広告表示側の対応は、基本的にこれのみです。

しかし、自前で広告を表示する(レアケース?)が必要であれば、以下対応も行います。

表示広告に広告ネットワークの情報を紐付ける

広告SDK側で実装されるケースがほとんどだと思いますが

広告表示(インプレッション)時に、後々インストールを検証する為の各キーを設定する必要があります。

広告インプレッション時に

class SKStoreProductViewController : UIViewController

のインスタンスメソッド

func loadProduct(withParameters parameters: [String : Any], completionBlock block: ((Bool, Error?) -> Void)? = nil)

を呼び出し、parametersに広告表示するアプリのIDや重複、不正防止用の署名などの値を指定します。

その後は

表示広告のタップが発生

presentで、SKStoreProductViewControllerのインスタンスを表示する
(AppStoreをアプリ内で表示)

という流れがひとつの形になると思います。

広告ネットワークでの対応

広告ネットワーク側で開発を行っている場合には対応が必要となってきます。

兎にも角にもまずは、Appleへ広告ネットワークとして登録し公式に認められる事が必要となります。

Appleへの広告ネットワーク登録

登録申請フォーム

Appleから登録が認められた後に

  • 広告ネットワークID(このIDを広告表示側アプリのinfo.plistに設定)が発行

  • 公開鍵の共有

  • 成果ポストバック用エンドポイントを共有
    Verifying an Install Validation Postback

というステップに進む事が可能になります。

三者間のコミュニケーションが大事

「SKAdNetwork」対応では、自分だけ(または相手だけ)が対応すれば良いという訳ではなく

  • 広告出稿側アプリ

  • 広告掲載側アプリ

  • 広告ネットワーク

の三者が足並みを揃えて対応する必要があります。

誰かが対応していないと機能しないので、上手く計測出来ない状況が簡単に起こります。

ただ、広告や計測に対するモチベーションも三者三様なので

ここに社内事情や意思決定のスピードのばらつき等がプラスされてしまうと、

状況は更に複雑(カオス)になってしまうのでは?と思います。

また

  • 成果のポストバック送信がリアルタイムではない(恐らくランダムで毎回違う)

という点もトラブルの原因特定を難しくさせそうです。


参考:
SKAdNetwork
Ad Network Install Validation Keys
Configuring the Participating Apps

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

(Swift 5対応)UILabelの文字に縁取り(袋文字)をする2つの方法

どうも、肩と背中の日焼けが激しすぎて服を着ないまま禁断の在宅勤務を行なっているiOSエンジニアのkaです。

文字の表示を簡単にネイティブの機能だけを使ってテレビ番組のテロップ風にしたいということでやってみました。

NSAttributedStringを使う方法

swift.swift
let stroke:UIColor = UIColor.black
let foreground:UIColor = UIColor.white
let width:CGFloat = 3.0

let strokeTextAttributes = [
    .strokeColor : stroke,
    .foregroundColor : foreground,
    .strokeWidth : width,
    .font : UIFont.boldSystemFont(ofSize: 20.0)
    ] as [NSAttributedString.Key : Any]
let string = NSMutableAttributedString.init(string: "縁取り文字", attributes: strokeTextAttributes)
label.attributedText = string

スクリーンショット 2020-08-20 11.56.36.png

CGContextを使う方法

UILabelのサブクラスを作ります

DecoreateLabel.swift
class DecorateLabel: UILabel {

    var strokeColor:UIColor = UIColor.white
    var strokeSize:CGFloat = 2.0

    override func drawText(in rect: CGRect) {
        if let context = UIGraphicsGetCurrentContext() { 
            let textColor = self.textColor

            context.setLineWidth(self.strokeSize)
            context.setLineJoin(CGLineJoin.round)
            context.setTextDrawingMode(.stroke)
            self.textColor = self.strokeColor
            super.drawText(in: rect)

            context.setTextDrawingMode(.fill)
            self.textColor = textColor
        }
        super.drawText(in: rect)
    }
}

swift.swift
let label = DecorateLabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
let stroke:UIColor = UIColor.black
let foreground:UIColor = UIColor.white
let width:CGFloat = 8.0
label.strokeColor = stroke
label.textColor = foreground
label.strokeSize = width
label.text = "縁取り文字"

スクリーンショット 2020-08-20 12.09.52.png

補足

NSAttributedStringを使う方法だと文字の内側に縁取り
CGContextを使う方法だと文字の外側に縁取りをします
場面によって使い分けましょう!

参考:

storyboard で UILabel を袋文字にする
UILabelの文字に枠をつけ、袋文字(縁取り文字)を作る
UILabelをNSAttributedStringで文字装飾(Swift 4対応)

終わりです

Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp

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

COCOA PODSでAlamofireとSwiftyJSONをインストールし、API通信。

Alamofireと、URLSessionの比較についてメモ書き。
SwiftyJSONも。

 はじめに

 COCOA PODS?

iOS開発での、パッケージ管理システム。
javascriptでいう、npmやyarn

 Alamofire?

iOS開発での、定番のAPIクライアント。
iOSのネットワーク通信処理をシンプルにする目的。

 SwiftyJSON?

Alamofireで取得したデータを、JSON形式で扱う。

 インストール手順

【Swift】COCOAPODSでAlamofireとSwiftyJSONをインストールし、API通信をする方法

 コード比較 (Webリクエスト)

Alamofireの、良いところ。

  • (data, response, error)が、(response)に集約
  • do-catchが不要
  • JSONSerialization.jsonObjectが不要
  • task.resume()が不要

 URLSession

func getRandomPersonUrlSession(id: Int, completion: @escaping PersonResponseCompletion) {

        guard let url = URL(string: "\(PERSON_URL)\(id)") else { return }

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

            guard error == nil else {
                debugPrint(error.debugDescription)
                return
            }


            guard let data = data else { return }

            do {
                let json = try JSONSerialization.jsonObject(with: data, options: [])
                guard let jsonAny = json as? [String: Any] else { return }

                let person = self.parsePersonManual(json: jsonAny)

                DispatchQueue.main.async {
                    completion(person)
                }
            } catch {
                debugPrint(error.localizedDescription)
                return
            }
        }
        task.resume()
    }

 Alamofire

func getRandomPersonAlamo(id: Int, completion: @escaping PersonResponseCompletion) {

        guard let url = URL(string: "\(PERSON_URL)\(id)") else { return }
        Alamofire.request(url).responseJSON { (response) in
            if let error = response.result.error {
                debugPrint(error.localizedDescription)
                completion(nil)
                return
            }

            guard let json = response.result.value as? [String: Any] else { return
                completion(nil) }
                let person = self.parsePersonManual(json: json)
                completion(person)
            }
    }

Alamofireを使うと、遥かに簡潔で見栄えが良くなりました。

 コード比較 (JSON解析)

SwiftyJSONの、いいところ

  • as? [String: Any]が不要。(=キャストが不要)

 ライブラリなし。

// Persing function using manual methods
    private func parsePersonManual(json: [String: Any]) -> Person {
        let name = json["name"] as? String ?? ""
        let height = json["height"] as? String ?? ""
        let mass = json["mass"] as? String ?? ""
        let hair = json["hair_color"] as? String ?? ""
        let birthYear = json["birth_year"] as? String ?? ""
        let gender = json["gender"] as? String ?? ""
        let homeWorldUrl = json["homeworld"] as? String ?? ""
        let filmUrls = json["films"] as? String ?? ""
        let vehicleUrls = json["vehicles"] as? [String] ?? [String]()
        let starshipUrls = json["starships"] as? [String] ?? [String]()

        return Person(name: name, height: height, mass: mass, hair: hair, birthYear: birthYear, gender: gender, homeWorldUrl: homeWorldUrl, filmUrls: filmUrls, vehicleUrls: vehicleUrls, starshipUrls: starshipUrls)
    }

 SwiftyJSON

 // Parsing with SwiftyJSON
    private func parsePersonSwifty(json: JSON) -> Person {
        let name = json["name"].stringValue
        let height = json["height"].stringValue
        let mass = json["mass"].stringValue
        let hair = json["hair_color"].stringValue
        let birthYear = json["birth_year"].stringValue
        let gender = json["gender"].stringValue
        let homeWorldUrl = json["homeworld"].stringValue
        let filmUrls = json["films"].stringValue
        let vehicleUrls = json["vehicles"].arrayValue.map({$0.stringValue})
        let starshipUrls = json["starships"].arrayValue.map({$0.stringValue})

        return Person(name: name, height: height, mass: mass, hair: hair, birthYear: birthYear, gender: gender, homeWorldUrl: homeWorldUrl, filmUrls: filmUrls, vehicleUrls: vehicleUrls, starshipUrls: starshipUrls)
    }

記述量はあまり変わらないけど、大量のas? [String: Any]が不要。
配列ではmapを使う。

おしまい。
何かあれば、追記します。

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

NCMB Swift コメント機能を理解して実装

コメントクラス作って、格納して実装するよりNCMBObject型のまま扱う方が勝手がいいのでNCMBObjectクラスでコメント機能実装します!

CommentViewController.swift
func loadComments() {
        comments = [Comment]()
        commentNcmbArray = [NCMBObject]()
        let query = NCMBQuery(className: "Comment")
        query?.whereKey("postId", equalTo: postId)
        query?.includeKey("user")
        query?.findObjectsInBackground({ (result, error) in
            if error != nil {
                KRProgressHUD.showError(withMessage:  error!.localizedDescription)
            } else {
                self.commentNcmbArray = result as![NCMBObject]
                for commentObject in result as! [NCMBObject] {
                    // コメントをしたユーザーの情報を取得
                    let user = commentObject.object(forKey: "user") as! NCMBUser
                    let userModel = User(objectId: user.objectId, userName: user.userName, displayName: user.object(forKey: "displayName") as! String)
                    userModel.displayName = user.object(forKey: "displayName") as? String

                    // コメントの文字を取得
                    let text = commentObject.object(forKey: "text") as! String
                    let userImagePath = "https://mbaas.api.nifcloud.com/2013-09-01/applications/自分のIDいれてね!/publicFiles/" + user.objectId

                    //                    cell.userImageView.kf.setImage(with: URL(string: userImageUrl), placeholder: UIImage(named: "human.png"), options: nil, progressBlock: nil)
                    // Commentクラスに格納
                    let comment = Comment(postId: self.postId, user: userModel, imageUrl: userImagePath,text: text, createDate: commentObject.createDate)
                    self.comments.append(comment)

                    // テーブルをリロード
                    self.commentTableView.reloadData()
                }

            }
        })
    }

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

NCMB SNSアプリ 選択したデータの削除

選択したデータの削除

ViewController.swift
 func didTapMenuButton(tableViewCell: UITableViewCell, button: UIButton) {
        let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        let deleteAction = UIAlertAction(title: "削除する", style: .destructive) { (action) in
            SVProgressHUD.show()
            let query = NCMBQuery(className: "Post")
            query?.getObjectInBackground(withId: self.posts[tableViewCell.tag].objectId, block: { (post, error) in
                if error != nil {
                    SVProgressHUD.showError(withStatus: error!.localizedDescription)
                } else {
                    // 取得した投稿オブジェクトを削除
                    post?.deleteInBackground({ (error) in
                        if error != nil {
                            SVProgressHUD.showError(withStatus: error!.localizedDescription)
                        } else {
                            // 再読込
                            self.loadTimeline()
                            SVProgressHUD.dismiss()
                        }
                    })
                }
            })
        }
        let reportAction = UIAlertAction(title: "報告する", style: .destructive) { (action) in
            SVProgressHUD.showSuccess(withStatus: "この投稿を報告しました。ご協力ありがとうございました。")
        }
        let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel) { (action) in
            alertController.dismiss(animated: true, completion: nil)
        }
        if posts[tableViewCell.tag].user.objectId == NCMBUser.current().objectId {
            // 自分の投稿なので、削除ボタンを出す
            alertController.addAction(deleteAction)
        } else {
            // 他人の投稿なので、報告ボタンを出す
            alertController.addAction(reportAction)
        }
        alertController.addAction(cancelAction)
        self.present(alertController, animated: true, completion: nil)
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NCMBデータストアからのデータ取得,適切なモデルへの格納

User.swift
import UIKit

class User :NSObject {
    var objectId: String
    var userName: String
    var displayName: String?
    var introduction: String   

    init(objectId: String, userName: String) {
        self.objectId = objectId
        self.userName = userName     
    }
}
Post.swift
import UIKit

class Post :NSObject{

    var objectId : String
    var user: User
    var imageUrl: String
    var text: String
    var createDate: Date
    var isLiked: Bool?
    var comment: [Comment]?
    var likeCount: Int = 0

    init(objectId: String, user: User, imageUrl: String, text: String, createDate: Date) {
        self.objectId = objectId
        self.user = user
        self.imageUrl = imageUrl
        self.text = text
        self.createDate = createDate
    }
}
ViewController.swift
func loadTimeline() {
        let query = NCMBQuery(className: "Post")
        guard let currentUser = NCMBUser.current() else {
            //guard文を使うとcurrentUserがnilだった時にログイン画面に戻る
            let storyboard = UIStoryboard(name: "SignIn", bundle: Bundle.main)
            let rootViewController = storyboard.instantiateViewController(identifier: "SignInNavigationController")
            UIApplication.shared.keyWindow?.rootViewController = rootViewController

            let ud  = UserDefaults.standard
            ud.set(false, forKey: "isLogin")
            ud.synchronize()

            return
        }
        // 降順
        query?.order(byDescending: "createDate")

        // 投稿したユーザーの情報も同時取得
        query?.includeKey("user")

        // フォロー中の人 + 自分の投稿だけ持ってくる
        //          query?.whereKey("user", containedIn: followings)

        // オブジェクトの取得
        query?.findObjectsInBackground({ (result, error) in
            if error != nil {
                SVProgressHUD.showError(withStatus: error!.localizedDescription)
            } else {
                // 投稿を格納しておく配列を初期化(これをしないとreload時にappendで二重に追加されてしまう)
                self.posts = [Post]()

                for postObject in result as! [NCMBObject] {

                    // ユーザー情報をUserクラスにセット
                    let user = postObject.object(forKey: "user") as! NCMBUser

                    // 退会済みユーザーの投稿を避けるため、activeがfalse以外のモノだけを表示
                    if user.object(forKey: "active") as? Bool != false {
                        // 投稿したユーザーの情報をUserモデルにまとめる
                        let userModel = User(objectId: user.objectId, userName: user.userName)
                        userModel.displayName = user.object(forKey: "displayName") as? String

                        // 投稿の情報を取得
                        let imageUrl = postObject.object(forKey: "imageUrl") as! String
                        let text = postObject.object(forKey: "text") as! String

                        // 2つのデータ(投稿情報と誰が投稿したか?)を合わせてPostクラスにセット
                        let post = Post(objectId: postObject.objectId, user: userModel, imageUrl: imageUrl, text: text, createDate: postObject.createDate)

                        // likeの状況(自分が過去にLikeしているか?)によってデータを挿入
                        let likeUsers = postObject.object(forKey: "likeUser") as? [String]
                        if likeUsers?.contains(currentUser.objectId) == true {
                            post.isLiked = true
                        } else {
                            post.isLiked = false
                        }

                        // いいねの件数
                        if let likes = likeUsers {
                            post.likeCount = likes.count
                        }

                        // 配列に加える
                        self.posts.append(post)
                    }
                }

                // 投稿のデータが揃ったらTableViewをリロード
                self.timeLineTableView.reloadData()
            }

        })
    }

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

Façadeパターン

元ネタ → ochococo/Design-Patterns-In-Swift

クラス図

image.png

図の引用元:Wikipedia: Facade パターン

概要

The facade pattern is used to define a simplified interface to a more complex subsystem.
Facadeパターンはより複雑なサブシステムへの簡略化されたインターフェイスを定義するために使用される。
クラス図のModuleA,B,Cmore complex subsystemに相当する。

サンプルコード

// Facade
final class Defaults {

    // ModuleA( more complex subsystem )
    private let defaults: UserDefaults

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
        // 実質.standard以外ないので、initを書かずに以下のようなプロパティを書いても良い気がする。
        // private let defaults: UserDefaults = .standard
    }

    // subscript: instance["hoge"]のような記述ででアクセスできるようになる
    // simplified interface
    subscript(key: String) -> String? {
        // ここのUserDefaultsにアクセスする際の癖をラップして簡略化
        get {
            return defaults.string(forKey: key)
        }   
        set {
            defaults.set(newValue, forKey: key)
        }
    }
}

// usage //

let storage = Defaults()

// Store
// 何かの台詞だろうか。
storage["Bishop"] = "Disconnect me. I’d rather be nothing"

// Read
storage["Bishop"]

クラス図との対応

  • Default = Facade
  • UserDefaults = ModuleA

考察

UserDefaultへの値の出し入れが少々癖がありより複雑なシステムであるところを、subscriptによってinstance[hoge]という書式で値の出し入れができる簡略化されたインタフェースに置き換えている。

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

SwiftアプリケーションにおいてJSON文字列で作業をする(エンコーディングとデコーディング)

この記事の内容は:
- (エンコーディング)Codable を使って Swift オブジェクトを JSON文字列にエンコードする
- (デコーディング) JSON文字列を JSONDecoder() でデコードする
- (デコーディング)SwiftyJSON を使って JSON文字列をデコードする

例では、仮に以下のプログラムの定義があるとしましょう

struct CatInformation {
    let id: Int
    let name: String
    let toys: [String]
    let status: CatStatus
}
struct CatStatus {
    let happy: Bool
    let playing: Bool
}

オブジェクトからJSON文字列を生成

我々は Swift オブジェクトCatInformationをJSON文字列に変換したいと考えています。

以下は Swift オブジェクトを定義したコードです:

let status = CatStatus(happy: true, playing: true)
let catInfo = CatInformation(id: 1, name: "ネコノヒー", toys: ["ペット小屋", "ぐるぐるタワー"], status: status)

構造体 Codable を作る

プログラム構造体の定義に Codable プロパティを追加することから始めます

struct CatInformation: Codable {
    let id: Int
    let name: String
    let toys: [String]
    let status: CatStatus
}

struct CatStatus: Codable {
    let happy: Bool
    let playing: Bool
}

JSONへの変換

do {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
    let jsonData = try encoder.encode(catInfo)
    let jsonString = String(data: jsonData, encoding: .utf8)
    print(jsonString)
} catch {
    print(error.localizedDescription)
}

そして、出力されたJSON文字列は以下の通りです

"{\n  \"status\" : {\n    \"happy\" : true,\n    \"playing\" : true\n  },\n  \"id\" : 1,\n  \"name\" : \"ネコノヒー\",\n  \"toys\" : [\n    \"ペット小屋\",\n    \"ぐるぐるタワー\"\n  ]\n}"

JSONデコーディング

この例では、以下のJSON文字列をウェブサイトのAPIコールから受け取ったとします。

{
    "id": 1,
    "name": "ネコノヒー",
    "toys": ["ペット小屋", "ぐるぐるタワー"],
    "status": {
        "happy": true,
        "playing": true
    }
}

この文字列をSwiftでどのようにデコードしますか。この記事では、このJSON文字列をデコードする2つの方法について考えます。
1. 新しい構造体を定義し、AppleのJSONDecoder()を使う
2. 既存の構造体を用い、AppleのJSONDecoder()を使う
2. SwiftyJSONのようなオープンソースフレームワークを使う

方法1 新しい構造体でJSONDecoder()を使用する(推奨される方法で簡単)

構造体を作成することができます。構造体はJSONファイル内のものと同じ変数名を含んでいなければなりません。例えば、JSONファイル内に id, name, toys, status があるとします。その場合、構造体の中の変数を id, name, toys, status と名付けなければなりません。

struct CatInformation: Decodable {
    let id: Int
    let name: String
    let toys: [String]
    let status: CatStatus
}

status は入れ子構造の変数(辞書型JSON)なので、それ用の別のプログラム構造を作る必要があります。

struct CatStatus: Decodable {
    let happy: Bool
    let playing: Bool
}

これでJSONメッセージをデコードすることができます

let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let catInfo = try! decoder.decode(CatInformation.self, from: jsonData)
print(catInfo)

そしてオブジェクトの配列をデコードします

let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let catInfo = try! decoder.decode(Array<CatInformation>.self, from: jsonData)
print(catInfo)

出力データは以下の通りです

[
 JSONdecode.CatInformation(catID: 1, catName: "ネコノヒー", catToys: ["ペット小屋", "ぐるぐるタワー"], catIsHappy: true, catIsPlaying: true),
 JSONdecode.CatInformation(catID: 2, catName: "ムギ", catToys: ["ぬいぐるみ"], catIsHappy: true, catIsPlaying: true),
 JSONdecode.CatInformation(catID: 3, catName: "レオ", catToys: [], catIsHappy: true, catIsPlaying: false)
]

上記のコードを使ってJSON文字列をデコードしてください。最適なコードのはずです。

既存の構造体に JSONDecoder 関数を使いたい方、オープンソースのソリューションを使いたい方は続きをご覧ください。

方法2 デコード可能な機能を既存の構成に追加する

上記の最初の例では、JSON変数の名前が Swift 変数に自動的にマッピングされます。ただし、名前が異なる場合は、 CodingKey プロパティを追加できます。

以下が既存の構成です:

struct CatInformation {
    let catID: Int
    let catName: String
    let catToys: [String]
    let catIsHappy: Bool
    let catIsPlaying: Bool
}

最初に、クラス/構成にデコード可能なプロパティを追加します

struct CatInformation: Decodable {
    ...
}

デコードキーを指定します

次に、JSON変数名を指定します。

struct CatInformation: Decodable {
    ...
    enum CatInformationKeys: String, CodingKey {
        case catID = "id"
        case catName = "name"
        case catToys = "toys"
        case catStatus = "status"
    }
    ...
}

これで、JSON文字列から、"happy" および "playing" プロパティが "status" 変数(ネストされた変数)内に格納されていることがわかります。したがって、 "happy" と "playing" を CodingKey の追加セットとして指定する必要があります。

struct CatInformation: Decodable {
    ...
    enum CatStatusKeys: String, CodingKey {
        case isHappy = "happy"
        case isPlaying = "playing"
    }
    ...
}

CatInformation を初期化します

まずは初期化関数を使ってみましょう

struct CatInformation: Decodable {
    ...
    init(from decoder: Decoder) throws {
        ...
    }
}

init関数内:

まず、JSON文字列全体をデコードします:

let values = try decoder.container(keyedBy: CatInformationKeys.self)

次に、入れ子になっていない変数をデコードします: catID, catName, catToys

catID = try values.decode(Int.self, forKey: .catID)
catName = try values.decode(String.self, forKey: .catName)
catToys = try values.decode([String].self, forKey: .catToys)

次に、nestedContainer(keyedBy:forKey:) を使用してステータスをデコードします

https://developer.apple.com/documentation/swift/keyeddecodingcontainer/2893204-nestedcontainer

let status = try values.nestedContainer(keyedBy: CatStatusKeys.self, forKey: .catStatus)
catIsHappy = try status.decode(Bool.self, forKey: .isHappy)
catIsPlaying = try status.decode(Bool.self, forKey: .isPlaying)

これでinit関数のコーディングは完了です。これでJSON文字列をデコードすることができます:

これがJSON文字列です:

let singleCatString =
"""
{
    "id": 1,
    "name": "ネコノヒー",
    "toys": ["ペット小屋", "ぐるぐるタワー"],
    "status": {
        "happy": true,
        "playing": true
    }
}
"""

これがデコード作業のコードです

let jsonData = singleCatString.data(using: .utf8)!
let decoder = JSONDecoder()
let catInfo = try! decoder.decode(CatInformation.self, from: jsonData)
print(catInfo)

通常、JSON 文字列は、このようなオブジェクトの配列を含んでいます:

let jsonString =
"""
[
    {
        "id": 1,
        "name": "ネコノヒー",
        "toys": ["ペット小屋", "ぐるぐるタワー"],
        "status": {
            "happy": true,
            "playing": true
        }
    },
    {
        "id": 2,
        "name": "ムギ",
        "toys": ["ぬいぐるみ"],
        "status": {
            "happy": true,
            "playing": true
        }
    },
    {
        "id": 3,
        "name": "レオ",
        "toys": [],
        "status": {
            "happy": true,
            "playing": false
        }
    },
]
"""

これで上記のJSON文字列をデコードすることができます:

let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let catInfo = try! decoder.decode(Array<CatInformation>.self, from: jsonData)
print(catInfo)

以下はプログラムの出力です

[
 JSONdecode.CatInformation(catID: 1, catName: "ネコノヒー", catToys: ["ペット小屋", "ぐるぐるタワー"], catIsHappy: true, catIsPlaying: true),
 JSONdecode.CatInformation(catID: 2, catName: "ムギ", catToys: ["ぬいぐるみ"], catIsHappy: true, catIsPlaying: true),
 JSONdecode.CatInformation(catID: 3, catName: "レオ", catToys: [], catIsHappy: true, catIsPlaying: false)
]

方法3 SwiftyJSON

SwiftyJSON は、JSONデータのデコードを支援するためのGithub上のオープンソースフレームワークです。

インストール方法

https://github.com/SwiftyJSON/SwiftyJSON#integration

SwiftyJSON

import SwiftyJSON
let data = jsonString.data(using: .utf8)!
let json = JSON(data)
if let catArrays = json.array {
    for catInformation in catArrays {
        if let catDictionary = catInformation.dictionary {
            let id = catDictionary["id"]?.int
            let name = catDictionary["name"]?.string
            let toys = catDictionary["toys"]?.array as? [String]
            //Decode the status
            if let status = catDictionary["status"]?.dictionary {
                let isHappy = status["happy"]?.bool
                let isPlaying = status["playing"]?.bool
                print("\(id), \(name), \(toys), \(isHappy), \(isPlaying)")
            }
        }
    }
}

ここで、.int は値を任意の整数にキャストします。.string はオプションのStringに値をキャストします。

そして、こちらがプログラムの出力です::

Optional(1), Optional("ネコノヒー"), nil, Optional(true), Optional(true)
Optional(2), Optional("ムギ"), nil, Optional(true), Optional(true)
Optional(3), Optional("レオ"), Optional([]), Optional(true), Optional(false)

こちらのウェブページにアクセスすると、私の公開されているQiitaの記事のリストをカテゴリー別にご覧いただけます。

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