muapps

iOSアプリ開発で得られた知見をメモ代わりに投稿します。

端末の向きに合わせて一部のViewのみ移動&回転させる

移動

複数の制約を設定しpriorityを変更することで制約を切り替える

@IBOutlet weak var targetViewPortraitCenterX: NSLayoutConstraint! // 初期priority: 999
@IBOutlet weak var targetViewPortraitCenterY: NSLayoutConstraint! // 初期priority: 999
@IBOutlet weak var targetViewLandscapeCenterX: NSLayoutConstraint! // 初期priority: 750
@IBOutlet weak var targetViewLandscapeCenterY: NSLayoutConstraint! // 初期priority: 750
targetViewPortraitCenterX.priority = UILayoutPriority(750)
targetViewPortraitCenterY.priority = UILayoutPriority(750)
targetViewLandscapeCenterX.priority = UILayoutPriority(999)
targetViewLandscapeCenterY.priority = UILayoutPriority(999)
回転
// 90°回転させる
let angle = 90 * CGFloat.pi / 180
self.targetView.transform = CGAffineTransform(rotationAngle: angle)
全体
class ExampleViewController: UIViewController {

    @IBOutlet weak var targetView: UIView!
    @IBOutlet weak var targetViewPortraitCenterX: NSLayoutConstraint! // 初期priority: 999
    @IBOutlet weak var targetViewPortraitCenterY: NSLayoutConstraint! // 初期priority: 999
    @IBOutlet weak var targetViewLandscapeCenterX: NSLayoutConstraint! // 初期priority: 750
    @IBOutlet weak var targetViewLandscapeCenterY: NSLayoutConstraint! // 初期priority: 750

     override func viewDidLoad() {
         super.viewDidLoad()
         // 端末の向き検知
         NotificationCenter.default.addObserver(self, selector: #selector(self.changedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil)
     }

     @objc func changedOrientation() {
          var angle: CGFloat = 0.0
          // 座標移動
          let orientation = UIDevice.current.orientation
          if orientation == .portrait {
               self.targetViewPortraitCenterX.priority = UILayoutPriority(999)
               self.targetViewPortraitCenterY.priority = UILayoutPriority(999)
               self.targetViewLandscapeCenterX.priority = UILayoutPriority(750)
               self.targetViewLandscapeCenterY.priority = UILayoutPriority(750)
               angle = 0.0
          } else if orientation == .landscapeLeft {
               self.targetViewPortraitCenterX.priority = UILayoutPriority(750)
               self.targetViewPortraitCenterY.priority = UILayoutPriority(750)
               self.targetViewLandscapeCenterX.priority = UILayoutPriority(999)
               self.targetViewLandscapeCenterY.priority = UILayoutPriority(999)
               angle = 90 * CGFloat.pi / 180
          }
          // 回転
          UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: {
               self.targetView.transform = CGAffineTransform(rotationAngle: angle)
          })
     }
}
さいごに

上の例はStoryboardを使用しているがコードでUIを組んでいても同じような方法でできると思う。
方法は他にもあるかもしれない。

Swiftの命名規則

命名について

コードのロジックを変更しなくても適切な命名をすればプログラムの動作を壊さずに可読性を改善することができます。

書籍「リーダブルコード」では第1部 表面上の改善 で命名方法が解説されています。

書籍「iOSアプリ設計パターン入門」にはモジュールやメソッド、変数において「責務をイメージできない名前をつけた途端に設計は腐敗への道を辿ります」とあります。
設計とは関心の分離によって複雑な問題を単純な問題の群として切り分けることです(解決すべき問題の領域=責務)。責務によって切り分けられた型を使用するプログラマも型が負う責務を理解して型を使用したり変更を加えていかなければ責務の境界が壊れていきます。この責務の境界を守っていくために責務を明確にイメージできる命名が必要です。

設計により責務が適切に分かれていることや可読性が高いことはコード理解を助け開発効率向上と不具合発生率を抑えることに大きく影響すると思うので適切な命名ができるように意識していきたいです。

Swift命名規則

Swiftの命名については以下にガイドラインが記載されています。

Swift.org - API Design Guidelines

最後に上記ガイドラインを一部日本語訳したものを記載します。

API Design Guidelines

基礎
  • 使用時の明瞭さが一番重要な目標。宣言(定義する時)だけでなく使用される時に明確か気をつける
  • 簡潔さよりも明確さが重要
命名

明確な使用を促進する

  • その命名が使用されているコードを読んでいる人への曖昧さを避けるために必要な全ての単語を含める
  • 不必要な言葉を省く

流暢な使用に努める

  • メソッド名と関数名は文法的に正しい英語のフレーズになるようにする
    ⭕️ x.capitalizingNouns()x.nounCapitalize()
  • 副作用*1に応じてメソッド名と関数名を命名する
    • 副作用がないものは名詞句にする x.distance(to: y)
    • 副作用のあるものは命令形の動詞句にする x.sort()
    • Mutatingメソッド/非Mutatingメソッドの名前は一貫したものにする
      • 動詞によって自然に記述される場合、
        • Mutatingメソッドに動詞の命令を使用 x.sort()
        • 非Mutatingメソッドには上記の動詞にedもしくはingをつける z = x.sorted()
      • 名詞によって自然に記述される場合、
        • Mutatingメソッドには名詞の前に「form」をつける y.formUnion(z)
        • 非Mutatingメソッドには名詞を使用 x = y.union(z)
  • Boolを返すメソッド名とプロパティ名は非Mutatingの場合アサーションとして読み取れるようにする x.isEmpty line1.intersects(line2)
  • それがなんであるか説明するプロトコル名は名詞にする Collection
  • 能力を表すプロトコルはable、ible、ingのいずれかをつけて命名する Equatable ProgressReporting
  • その他のタイプ、プロパティ、変数、および定数の名前は名詞にする

*1 副作用 - メソッドの呼び出しによってオブジェクトの状態、または別のオブジェクトの状態が変化すること

慣習

一般的な規則

  • 大文字と小文字の規則に従うこと。タイプとプロトコルの名前は UpperCamelCaseにする。他のすべてはlowerCamelCaseにする。

    • アメリカ英語ですべて大文字として一般的に表示される頭字語と頭字語は、大文字と小文字の規則に従って一律に大文字または小文字にする
      var utf8Bytes: [UTF8.CodeUnit] var isRepresentableAsASCII = true

SwiftLint導入

前提

環境:
M1 Mac
Xcode13.2.1
SwiftLint0.46.3

インストール

SwiftLintのreadme
https://zgithub.com/realm/SwiftLint

まずMintでインストールしようとしたが以下のエラーが出た。
warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint

上記はBuild Phasesに記載したコードのelseに入った結果の警告文。
解決には以下が何かしら参考になるかもしれない。

今回はSwiftLintを試すことが目的なのでHomebrewでのインストールに変更した。

運用方針

SwiftLintは初めからデフォルトルールが有効になっている。

  • disabled_rules:
    デフォルトで有効になっているルールの中で無効にするものを指定する
  • opt_in_rules:
    デフォルトで有効になっていないルールで有効にしたいものを指定する

まずデフォルトで実行して必要ないものを無効にする、次にopt_inを眺めて有効にしたいものを見つけるとかがいいのかなと思った。

関連リンク

Clousure - クロージャ

公式ドキュメントを読んでクロージャの整理。

docs.swift.org

クロージャはコード内で受け渡しできる自己完結型のブロック。
クロージャは定数および変数への参照をそれらが定義されているコンテキストからキャプチャして保存できる。

グローバル関数とネストされた関数はクロージャの特殊なケース。 クロージャの形式は以下の3つ。

クロージャ

外からクロージャを渡す場合

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

クロージャ式の構文の一般的な形式

{ (parameters) -> return type in
    statements
}

一般的なクロージャ式の構文そのままで書くと以下のように書ける。

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

この時Swiftは引数の型と戻り値の型を推測できるので以下のように書ける。

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Swiftはインラインクロージャに短縮引数名を自動的に提供する。 inキーワードも省略できる。

reversedNames = names.sorted(by: { $0 > $1 } )

関数の末尾のクロージャ(トレーリングクロージャ)

関数の最後の引数としてクロージャ式を関数に渡す必要があるとき末尾のクロージャとして記述できる。末尾のクロージャ構文を使用する場合引数ラベルは記述しない。

sortedメソッドを末尾のクロージャを使って書くと

reversedNames = names.sorted() { $0 > $1 }

クロージャ式が関数またはメソッドの唯一の引数で末尾のクロージャとして書く場合関数またはメソッドの名前の後に括弧のペアは記述しなくていい。

reversedNames = names.sorted { $0 > $1 }

末尾のクロージャは、クロージャが十分に長く、1行にインラインで書き込むことができない場合に最も役に立つ。

関数が複数のクロージャを取る場合

関数を呼び出すとき最初の末尾のクロージャの引数ラベルを省略し、残りの末尾のクロージャにラベルを付ける。

定義

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

呼び出し

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

値をキャプチャする

クロージャは自身が定義されている周囲のコンテキストから定数と変数をキャプチャできる。定数と変数を定義した元のスコープが存在しなくなった場合でも、ボディからこれらの定数と変数の値を参照および変更できる。

ネストされた関数の場合

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

incrementer()はrunningTotalとamountを周囲からキャプチャする。

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

参照によるキャプチャはrunningTotalとamountがmakeIncrementerの呼び出しが終了した時消えないこととrunningTotalが次にincrementerが呼ばれたときに使用可能なことを保証する。

次のincrementByTenはincrementer関数を参照する。incrementer関数は呼び出される度にrunningTotalに10を足す。

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

2つ目のincrementerを作ると先述のものとは別の新しいrunningTotalへの参照を持つ。

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

クロージャエスケープする

関数の引数として渡されたクロージャが関数が返された後に呼ばれるときクロージャが関数をエスケープすると言う。エスケーピングクロージャがselfをキャプチャするときは明示的にselfを記載するかキャプチャリストに含める必要がある。selfを明示的に記載することで意図を表現でき参照サイクルがないことを確認するように促される。

// エスケーピングクロージャ
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

// 通常のクロージャ
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // xを明示的にキャプチャする
        someFunctionWithNonescapingClosure { x = 200 } // xを暗黙的にキャプチャする
    }
}
キャプチャリストにselfを含める場合の書き方
// someFunctionWithEscapingClosure { [self] in x = 100 }

※オプショナル型はデフォルトで@escapingになる Optional Non-Escaping Closures – Ole Begemann

構造体・列挙体の場合

selfが構造体または列挙体のインスタンスである時はselfを常に暗黙的に参照できる。ただしこの時エスケーピングクロージャはselfへのmutableな参照をキャプチャできない。構造体と列挙体は共有された可変性を許可していない。

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

上記でsomeFunctionWithEscapingClosureはmutationgメソッドの中にありselfはmutable なのでエラーになる。

オートクロージャ

オートクロージャとは関数の引数として渡される式をラップするために自動的に作成されるクロージャのこと。引数をとらず、呼び出されるとその中にラップされている式の値を返す。

オートクロージャを使用すると引数として関数を取る時に{}を使ってクロージャとして書かずに通常の引数のように書ける。

@autoclosureを使わずに書く場合

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

@autoclosureを使って書く場合

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

オートクロージャを使用すると、クロージャを呼び出すまで内部のコードが実行されないため評価を遅らせることができる。評価の遅延はコードがいつ評価されるかを制御できるため、副作用があるコードや計算コストが高いコードに役立つ。

エスケープを許可するオートクロージャが必要な場合は@autoclosure@escaping属性の両方を使用する。

Dispatchqueueの挙動整理

以下の記事を参考にDispatchqueueの種類ごとにPlaygroundで実行してみた

dev.classmethod.jp

実行環境:
Xcode13.0
Swift5.2

メインキュー

sync(同期)

// メインキューは直列(前のタスクが完了次第、次のタスクが実行される)
let mainQueue = DispatchQueue.main
// syncを使うと呼び出し元がブロックされる
print("start")
for i in 0...5 {
    mainQueue.sync() {
        print(i)
    }
}
print("end")

実行結果

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

async(非同期)

// 非同期なので呼び出し元がブロックされずstart、endが最初に出力される
// 直列なので番号は順番に出力される
print("start")
for i in 0...5 {
    mainQueue.async() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
end
0
1
2
3
4
5

グローバルキュー

sync(同期)

// グローバルキューは並列(前のタスクの処理状況に関わらず、次のタスクが実行される)
let grobalQueue = DispatchQueue.global(qos: .default)

// 同期なので呼び出し元がブロックされてendが最後に出力される
// 並列だが同期なので番号は順番に出力される
print("start")
for i in 0...5 {
    grobalQueue.sync() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
0
1
2
3
4
5
end

async(非同期)

// 非同期なので呼び出し元がブロックされずstart、endが最初に出力される
// 並列なので番号が出力される順番はバラバラ
print("start")
for i in 0...5 {
    grobalQueue.async() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
end
4
2
3
0
1
5

自分でDispatchQueueを用意する場合

// 直列
let serialQueue = DispatchQueue(label: "serialQueue")
// 並列
let parallelQueue = DispatchQueue(label: "parallelQueue", attributes: .concurrent)

同期および非同期での実行結果はメインキュー、グローバルキューの結果から推察できる通り。
メインキューではエラーになったが直列で同期の場合は以下のようになる。

print("start")
for i in 0...5 {
    serialQueue.sync() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
0
1
2
3
4
5
end

Androidアプリ開発時に便利なツール

前提

端末: Mac

ツール

Android File Transfer

Mac-Android間のファイル転送。AndroidのUSB接続が「充電」になっている場合「ファイル転送」をONにする必要あり

www.android.com

Android Tool

Androidのスクショや画面収録

github.com