- 投稿日:2020-07-06T22:59:21+09:00
UserDefaultってすっごくシンプル!?(追加と取得)
UserDefaultの書込み
って本当は簡単なのかもしれない。と思い、簡略化して書いてみました。
View1Controller.swiftimport UIKit class ViewController: UIViewController,UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! var emailTextField = "" override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self } @IBAction func ButtonAction(_ sender: Any) UserDefaults.standard.set(emailTextField.text, forKey: "userMail")UserDefault.standard.set で保存(キー値はここでは"userMail")
これだけUserDefaultの取得も全然簡単!?
View2Controller.swiftimport UIKit class View2Controller: UIViewController,UIImagePickerControllerDelegate,UINavigationControllerDelegate { @IBOutlet weak var acountEmail: UILabel! var userMail = String() override func viewDidLoad() { super.viewDidLoad() userMail = UserDefaults.standard.object(forKey: "userMail") as! String acountEmail.text = userMail }取得もこれだけ!
ちゃんと理解しようとせず、難しそうなもんだと決め付けていました。。
もちろん応用していこうと思うと、ここから複雑になってはいきますが苦手意識が少しでもなくなればなと思います!
- 投稿日:2020-07-06T20:59:04+09:00
SwiftUI で Animatable なシェイプを作ってみる
シェイプにアニメーションをつけてみました
SwiftUI の勉強をかねて、以前の記事で作成した SwiftUI のシェイプにアニメーションをつけてみました。
— takehito-koshimizu (@takehitokoshim1) July 6, 2020 — takehito-koshimizu (@takehitokoshim1) July 6, 2020環境
- Xcode Version 11.5 (11E608c)
参考記事
今回はこちらの記事を参考にして、以前作成したシェイプにアニメーションをつけてみました。
動画が非常にわかりやすくて参考になりました。Animating simple shapes with animatableData - a free Hacking with iOS: SwiftUI Edition tutorial
https://www.hackingwithswift.com/books/ios-swiftui/animating-simple-shapes-with-animatabledataAnimating complex shapes with AnimatablePair - a free Hacking with iOS: SwiftUI Edition tutorial
https://www.hackingwithswift.com/books/ios-swiftui/animating-complex-shapes-with-animatablepair
Shape
はもともとAnimatable
まずは Xcode で
Shape
の定義にジャンプしてみます。@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public protocol Shape : Animatable, View { /// Describes this shape as a path within a rectangular frame of reference. /// /// - Parameter rect: The frame of reference for describing this shape. /// - Returns: A path that describes this shape. func path(in rect: CGRect) -> Path }実は
Shape
はもともとAnimatable
のサブタイプであることがわかります。
更に、Animatable
にジャンプしてみます。/// A type that can be animated @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public protocol Animatable { /// The type defining the data to be animated. associatedtype AnimatableData : VectorArithmetic /// The data to be animated. var animatableData: Self.AnimatableData { get set } }
Animatable
はanimatableData
という1つのプロパティを持ち、
これを実装することで、アニメーションをつけることができみたいです。自作のシェイプにアニメーションをつける
以前の記事で作成した
StarShape
のプロパティsmoothness
にアニメーションをつけてみます。import SwiftUI struct StarShape: Shape { var vertex: UInt = 5 var smoothness: Double = 0.5 var rotation: CGFloat = -.pi/2 var animatableData: Double { get { smoothness } set { smoothness = newValue } } func path(in rect: CGRect) -> Path { Path { path in let points: [CGPoint] = StarParameters(vertex: vertex, smoothness: smoothness) .center(x: rect.midX, y: rect.midY) .radius(min(rect.midX, rect.midY)) .rotated(by: rotation) .cgPoints path.move(to: points.first!) points.forEach { point in path.addLine(to: point) } path.closeSubpath() } } }ポイントはここ。
var animatableData: Double { get { smoothness } set { smoothness = newValue } }
animatableData
のget
,set
でプロパティを指定します。
たったこれだけです。早速、このシェイプを使ってみます。
import SwiftUI private let style = LinearGradient( gradient: Gradient(colors: [ Color(red: 239/255, green: 120.0/255, blue: 221/255), Color(red: 239/255, green: 172.0/255, blue: 120/255) ]), startPoint: UnitPoint(x: 0.5, y: 0), endPoint: UnitPoint(x: 0.5, y: 0.6) ) struct StarView: View { /// 頂点の数 var vertex: UInt = 5 /// 滑らかさ @State var smoothness: Double = 0.5 var body: some View { let shape = StarShape(vertex: vertex, smoothness: smoothness) return ZStack { shape.fill(style) shape.stroke(Color.black, lineWidth: 4) } .aspectRatio(1.1, contentMode: .fit) .onTapGesture { withAnimation { self.smoothness = Double(Int(self.smoothness * 10 + 1) % 5 + 5)/10 } } } }ビューのタップジェスチャーで
smoothness
を書換えて、再描画します。
アニメーションさせたいので、withAnimation
関数で囲んでいます。複数のプロパティにアニメーションをつける
StarShape
のプロパティvertex
もアニメーション可能にしてみます。
VectorArithmetic
に適合しているAnimatablePair
を利用します。import SwiftUI struct StarShape: Shape { var vertex: UInt = 5 var smoothness: Double = 0.5 var rotation: CGFloat = -.pi/2 var animatableData: AnimatablePair<Double, Double> { get { AnimatablePair(Double(vertex), smoothness) } set { vertex = UInt(newValue.first) smoothness = newValue.second } } /* 以下省略 */ }このようなペアを返すことで
animatableData
に複数のプロパティを参照させることができます。
次のように、型パラメータにAnimatablePair
を指定することで、3つ以上プロパティをアニメーション可能にすることもできるみたいです。// 3つ AnimatablePair<Double, AnimatablePair<Double, Double>> // 4つ AnimatablePair<Double, AnimatablePair<Double, AnimatablePair<Double, Double>>> // 5つ AnimatablePair<Double, AnimatablePair<Double, AnimatablePair<Double, AnimatablePair<Double, Double>>>>このシェイプを使って、頂点の増減にもアニメーションをかけてみます。
サンプルコードは次の通りです。import SwiftUI private let style = LinearGradient( gradient: Gradient(colors: [ Color(red: 239/255, green: 120.0/255, blue: 221/255), Color(red: 239/255, green: 172.0/255, blue: 120/255) ]), startPoint: UnitPoint(x: 0.5, y: 0), endPoint: UnitPoint(x: 0.5, y: 0.6) ) struct StarView: View { /// 頂点の数 @State var vertex: UInt = 5 /// 滑らかさ @State var smoothness: Double = 0.5 var body: some View { let shape = StarShape(vertex: vertex, smoothness: smoothness) return ZStack { shape.fill(style) shape.stroke(Color.black, lineWidth: 4) } .aspectRatio(1.1, contentMode: .fit) .onTapGesture { withAnimation { self.vertex = ((self.vertex + 3) % 5) + 5 self.smoothness = Double(Int(self.smoothness * 10 + 1) % 5 + 5)/10 } } } }まとめ
単純な図形であれば、簡単にアニメーションの実装ができることがわかりました。
- アニメーション可能なビューにするためには
Animatable
に適合するShape
はもともとAnimatable
AnimatablePair
で複数の属性をアニメーション可能にすることができる
- 投稿日:2020-07-06T19:26:03+09:00
[Swift] コードで作成したUITextFieldに余白を作る
- 投稿日:2020-07-06T15:38:42+09:00
TCAのテストコードについて解説
『Swiftによるアプリ開発のためのComposable Architectureがすごく良いので紹介したい』で紹介したThe Composable Architecture(TCA)にはテスト用の複数の型もあり、CaseStudiesにサンプルコードのテストが書かれているのでそれについてざっくり書いておきます。
TCAのCaseStudiesでテストしていること
- Reducerの処理をテストし、Stateが意図通り変更されたかを期待値と比べて検証する
- Reducerの処理が意図通り書かれているか
- Stateの構造が意図通りか
テスト用の型
- TestStore
- 役割
- Reducerをテストするための型
- テストしたいReducerをセットしたりテスト用のスケジューラなどに差し替える
- 期待値との比較を行えるメソッドを持つ
- メソッド
- send
- アクションを指定
- do
- スケジューラを進めたり。アサーション間で行われる作業を書く。
- receive
- sendでさらに実行されて受け取ったReducerのアクション
- TestScheduler
- 役割
- 実時間ではなくテスト用の時間で検証する
- 実時間だと細かな時間を気にしないといけないし
- 実際にその時間通りに動くようにする場合、長い時間のテストならその分だけ時間がかかる
- テスト用の時間とは仮想的な単位
- そもそもEffectがPublisherなので時系列にアクセスする必要がありスケジューラが必要
前提としてReducerはアクションが起こると、Stateを書き換えたりEffectを起こしたり、再度アクションを呼び出したりする。
TestStoreのsendメソッドはReducerに送るアクションで、Reducerがその後アクションをするならreceiveを書くようにし、Stateの状態を検証する。
実際のテストコード
02-Effects-Basics.swift
- これなに?
- カウンタとAPIアクセス
- なにがわかる?
- TestStoreの基本
import ComposableArchitecture import XCTest @testable import SwiftUICaseStudies class EffectsBasicsTests: XCTestCase { let scheduler = DispatchQueue.testScheduler func testCountDown() { // storeがテスト用storeのデータを用意する。 // テスト用の依存物を注入し、依存関係を解決できる。 let store = TestStore( initialState: EffectsBasicsState(), // Stateも本番用のStateをのまま reducer: effectsBasicsReducer, // Reducerも本番用 environment: EffectsBasicsEnvironment( // Environmentはテスト用 mainQueue: self.scheduler.eraseToAnyScheduler(), // メインキュー numberFact: { _ in fatalError("Unimplemented") } // Effectは実行したらエラー ) ) // 全体の流れについて検証できる。各ステップでは状態が予想通りに変化したことを証明する。 store.assert( // アクションを呼び出す .send(.incrementButtonTapped) { // Stateが期待値通りか検証 $0.count = 1 }, .send(.decrementButtonTapped) { $0.count = 0 }, // このサンプルの .decrementButtonTappedはなぜか デクリメントした後にインクリメントする。 // 時間を進めてその結果を受け取ろう。 .do { self.scheduler.advance(by: 1) }, スケジューラの1ステップ先までを取得 .receive(.incrementButtonTapped) { $0.count = 1 } ) } func testNumberFact() { let store = TestStore( initialState: EffectsBasicsState(), reducer: effectsBasicsReducer, environment: EffectsBasicsEnvironment( mainQueue: self.scheduler.eraseToAnyScheduler(), numberFact: { n in Effect(value: "\(n) is a good number Brent") } ) ) store.assert( .send(.incrementButtonTapped) { $0.count = 1 }, // ファクトボタンをタップすると、エフェクトからの応答が返ってくることをテスト。 .send(.numberFactButtonTapped) { $0.isNumberFactRequestInFlight = true }, // Reducer内部でreceive(on: environment.mainQueue)してるのでスケジューラの時間をすすめる .do { self.scheduler.advance() }, // .success("1 is a good number Brent") はテストデータ .receive(.numberFactResponse(.success("1 is a good number Brent"))) { $0.isNumberFactRequestInFlight = false $0.numberFact = "1 is a good number Brent" // 要求する期待値 } ) } }期待値はクロージャでキャプチャさせるが、それと検証するテストデータはreceiveを使って渡す。
02-Effects-TimersTests
- これなに?
- 1秒ごとのタイマーのテスト
- なにがわかる?
- スケジューラについてちょっと分かる。
import ComposableArchitecture import XCTest @testable import SwiftUICaseStudies class TimersTests: XCTestCase { let scheduler = DispatchQueue.testScheduler func testStart() { let store = TestStore( initialState: TimersState(), reducer: timersReducer, environment: TimersEnvironment( mainQueue: self.scheduler.eraseToAnyScheduler() ) ) store.assert( // アクションを送る .send(.toggleTimerButtonTapped) { // Stateが書き換わるかどうか検証 $0.isTimerActive = true }, .do { self.scheduler.advance(by: 1) }, .receive(.timerTicked) { $0.secondsElapsed = 1 }, .do { self.scheduler.advance(by: 5) }, // 5ステップまでの取得 .receive(.timerTicked) { $0.secondsElapsed = 2 }, .receive(.timerTicked) { $0.secondsElapsed = 3 }, .receive(.timerTicked) { $0.secondsElapsed = 4 }, .receive(.timerTicked) { $0.secondsElapsed = 5 }, .receive(.timerTicked) { $0.secondsElapsed = 6 }, .send(.toggleTimerButtonTapped) { $0.isTimerActive = false } ) } }02-Effects-LongLivingTests
イベントとして、
UIApplication.userDidTakeScreenshotNotification
のNotificationを取得する場合。外部からのイベントをSubjectとして呼び出されるようにする。import Combine import ComposableArchitecture import XCTest @testable import SwiftUICaseStudies class LongLivingEffectsTests: XCTestCase { func testReducer() { // A passthrough subject to simulate the screenshot notification let screenshotTaken = PassthroughSubject<Void, Never>() let store = TestStore( initialState: .init(), reducer: longLivingEffectsReducer, environment: .init( userDidTakeScreenshot: Effect(screenshotTaken) ) ) store.assert( .send(.onAppear), // Simulate a screenshot being taken .do { screenshotTaken.send() }, .receive(.userDidTakeScreenshotNotification) { $0.screenshotCount = 1 }, .send(.onDisappear), // Simulate a screenshot being taken to show not effects // are executed. .do { screenshotTaken.send() } ) } }02-Effects-CancellationTests
Effectが動作していないことを確認する
func testTrivia_CancelButtonCancelsRequest() throws { let store = TestStore( initialState: .init(), reducer: effectsCancellationReducer, environment: .init( mainQueue: self.scheduler.eraseToAnyScheduler(), trivia: { n in Effect(value: "\(n) is a good number Brent") } ) ) // キャンセルされて \(n) is a good number Brent を取得しなければ成功 store.assert( .send(.triviaButtonTapped) { $0.isTriviaRequestInFlight = true }, .send(.cancelButtonTapped) { $0.isTriviaRequestInFlight = false }, .do { self.scheduler.run() } ) }