20200706のSwiftに関する記事は4件です。

UserDefaultってすっごくシンプル!?(追加と取得)

UserDefaultの書込み

って本当は簡単なのかもしれない。と思い、簡略化して書いてみました。

View1Controller.swift
import 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.swift
import 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
    }

取得もこれだけ!
ちゃんと理解しようとせず、難しそうなもんだと決め付けていました。。
もちろん応用していこうと思うと、ここから複雑になってはいきますが苦手意識が少しでもなくなればなと思います!

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

SwiftUI で Animatable なシェイプを作ってみる

シェイプにアニメーションをつけてみました

SwiftUI の勉強をかねて、以前の記事で作成した SwiftUI のシェイプにアニメーションをつけてみました。

環境

  • 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-animatabledata

Animating 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 }
}

AnimatableanimatableData という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 }
}

animatableDataget, 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 で複数の属性をアニメーション可能にすることができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] コードで作成したUITextFieldに余白を作る

はじめに

掲題の通りに、UITextFieldに余白を作る方法。

UITextFieldを継承したカスタムクラスを作成して、余白を作るとという記事が多かったがもっと簡単な物があったので備忘録的に残す。

以下を設定するだけ

textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
// widthはお好みで設定してください
textField.leftViewMode = UITextField.ViewMode.always

以上!!!

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

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の状態を検証する。

実際のテストコード

https://github.com/pointfreeco/swift-composable-architecture/tree/0.6.0/Examples/CaseStudies/SwiftUICaseStudies

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()
      }
    )
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む