- 投稿日:2020-08-20T22:54:09+09:00
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); } }
- 投稿日:2020-08-20T21:11:14+09:00
【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 } }
- 投稿日:2020-08-20T19:03:13+09:00
【今日からできる】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())ActivityIndicatorstruct 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のテストをやってみます。
ViewModelprotocol 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
階層化されたSwiftUIのViewから、どうやって中身を取り出していくかという苦労が綴られていて面白いです。 ↩
- 投稿日:2020-08-20T13:30:33+09:00
長押し続けている間繰り返し処理をする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を使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-08-20T13:14:54+09:00
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から登録が認められた後に
広告ネットワークID(このIDを広告表示側アプリのinfo.plistに設定)が発行
公開鍵の共有
成果ポストバック用エンドポイントを共有
Verifying an Install Validation Postbackというステップに進む事が可能になります。
三者間のコミュニケーションが大事
「SKAdNetwork」対応では、自分だけ(または相手だけ)が対応すれば良いという訳ではなく
広告出稿側アプリ
広告掲載側アプリ
広告ネットワーク
の三者が足並みを揃えて対応する必要があります。
誰かが対応していないと機能しないので、上手く計測出来ない状況が簡単に起こります。
ただ、広告や計測に対するモチベーションも三者三様なので
ここに社内事情や意思決定のスピードのばらつき等がプラスされてしまうと、
状況は更に複雑(カオス)になってしまうのでは?と思います。
また
- 成果のポストバック送信がリアルタイムではない(恐らくランダムで毎回違う)
という点もトラブルの原因特定を難しくさせそうです。
参考:
SKAdNetwork
Ad Network Install Validation Keys
Configuring the Participating Apps
- 投稿日:2020-08-20T12:18:19+09:00
(Swift 5対応)UILabelの文字に縁取り(袋文字)をする2つの方法
どうも、肩と背中の日焼けが激しすぎて服を着ないまま禁断の在宅勤務を行なっているiOSエンジニアのkaです。
文字の表示を簡単にネイティブの機能だけを使ってテレビ番組のテロップ風にしたいということでやってみました。
NSAttributedStringを使う方法
swift.swiftlet 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 = stringCGContextを使う方法
UILabelのサブクラスを作ります
DecoreateLabel.swiftclass 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.swiftlet 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 = "縁取り文字"補足
NSAttributedStringを使う方法だと文字の内側に縁取り
CGContextを使う方法だと文字の外側に縁取りをします
場面によって使い分けましょう!参考:
storyboard で UILabel を袋文字にする
UILabelの文字に枠をつけ、袋文字(縁取り文字)を作る
UILabelをNSAttributedStringで文字装飾(Swift 4対応)終わりです
Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp
- 投稿日:2020-08-20T12:01:34+09:00
COCOA PODSでAlamofireとSwiftyJSONをインストールし、API通信。
Alamofireと、
URLSessionの比較についてメモ書き。
SwiftyJSONも。はじめに
COCOA PODS?
iOS開発での、パッケージ管理システム。
javascriptでいう、npmやyarnAlamofire?
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を使う。おしまい。
何かあれば、追記します。
- 投稿日:2020-08-20T11:57:03+09:00
NCMB Swift コメント機能を理解して実装
コメントクラス作って、格納して実装するよりNCMBObject型のまま扱う方が勝手がいいのでNCMBObjectクラスでコメント機能実装します!
CommentViewController.swiftfunc 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() } } }) }
- 投稿日:2020-08-20T11:52:17+09:00
NCMB SNSアプリ 選択したデータの削除
選択したデータの削除
ViewController.swiftfunc 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) }
- 投稿日:2020-08-20T11:48:46+09:00
NCMBデータストアからのデータ取得,適切なモデルへの格納
User.swiftimport 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.swiftimport 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.swiftfunc 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() } }) }
- 投稿日:2020-08-20T09:05:55+09:00
Façadeパターン
元ネタ → ochococo/Design-Patterns-In-Swift
クラス図
図の引用元:Wikipedia: Facade パターン
概要
The facade pattern is used to define a simplified interface to a more complex subsystem.
Facadeパターンはより複雑なサブシステムへの簡略化されたインターフェイスを定義するために使用される。
クラス図のModuleA,B,Cがmore 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=FacadeUserDefaults=ModuleA考察
UserDefaultへの値の出し入れが少々癖がありより複雑なシステムであるところを、
subscriptによってinstance[hoge]という書式で値の出し入れができる簡略化されたインタフェースに置き換えている。
- 投稿日:2020-08-20T06:08:33+09:00
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,catToyscatID = 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 SwiftyJSONlet 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)




