20200805のSwiftに関する記事は14件です。

【Swift】UIButtonをタップ(isHighlighted)した時にBackgroundColorを変更する

UIButtonのタップアクションに合わせて文字の色を変更するのは簡単ですが、背景の色を変えるのは少し面倒です。

main.swift
// CustomButtonを作成する
@IBOutlet weak var button: CustomButton!

カスタムボタンのクラス

class CustomButton: UIButton {
    override open var isHighlighted: Bool {
        // ここでisHighlightedを元にbackgroundColorを設定する
        didSet {
            backgroundColor = isHighlighted ? .gray : .white
        }
    }
}

これでボタンのタップアクションに合わせて背景の色を変えることができます?

余談

もちろん同じ要領でisSelectedの時も色の変更が可能。
プロジェクトでレイアウトを統一する時は共通コンポーネントとして用意しておくと便利ですね。

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

日本人のための SwiftFormat【ルール編】

ルール一覧

SwiftFormat の Rules 一覧を日本語でまとめました。

andOperator

if, guard, whileの条件分岐の際の条件式で && を使う場合はカンマを使うようにする。

サンプル

- if true && true {
+ if true, true {

anyObjectProtocol

プロトコルの定義で、classAnyObject に変更する。

class の代わりに AnyObject を使用するガイドラインは Swift4.1から導入されたので、anyObjectProtocol
のルールは Swift4.1以上でない限り無効になる。

サンプル

- protocol Foo: class {}
+ protocol Foo: AnyObject {}

blankLinesAroundMark

MARK:コメントの前後に空白行を入れる。

サンプル

func foo() {
  // foo
}
// MARK: bar
func bar() {
  // bar
}

func foo() {
  // foo
}
+
// MARK: bar
+
func bar() {
  // bar
}

blankLinesAtEndOfScope

スコープの末尾にある空白行を削除する。

サンプル

func foo() {
  // foo
-
}

func foo() {
  // foo
}
array = [
  foo,
  bar,
  baz,
-
]

array = [
  foo,
  bar,
  baz,
]

blankLinesAtStartOfScope

スコープの先頭にある空白行を削除する。

サンプル

func foo() {
-
  // foo
}

func foo() {
  // foo
}
array = [
-
  foo,
  bar,
  baz,
]

array = [
  foo,
  bar,
  baz,
]

blankLinesBetweenScopes

class, struct, enum, extension, protocol, function の宣言の前に空白行を
入れる。

サンプル

func foo() {
  // foo
}
func bar() {
  // bar
}
var baz: Bool
var quux: Int

func foo() {
  // foo
}
+
func bar() {
  // bar
}
+
var baz: Bool
var quux: Int

braces

字下げスタイルを選択する。(K&R or Allman)
デフォルトでは、K&Rが適用される。

option description
--allman allmanインデントスタイルを使用: "true" or "false"(default)

サンプル

- if x
- {
    // foo
  }
- else
- {
    // bar
  }

+ if x {
    // foo
  }
+ else {
    // bar
  }

consecutiveBlankLines

連続する空白行を1行にまとめる。

サンプル

func foo() {
  let x = "bar"
-

  print(x)
}

func foo() {
  let x = "bar"

  print(x)
}

consecutiveSpaces

連続するスペースを1つのスペースをに置き換える。

サンプル

- let     foo = 5
+ let foo = 5

duplicateImports

重複する import 文を削除する。

サンプル

import Foo
import Bar
- import Foo
import B
#if os(iOS)
  import A
-   import B
#endif

elseOnSameLine

else, catch, while キーワードのポジションを "同じ行" または "次の行" に調整する。

option description
--elseposition else/catchのポジション: "same-line"(Default) or "next-line"
--guardelse guard elseのポジション: "same-line" or "next-line" or "auto"(default)

emptyBraces

カッコ内の空白を削除する。

サンプル

- func foo() {
-
- }

+ func foo() {}

fileHeader

全てのファイルに指定されたソースファイルヘッダーテンプレートを使用する。

option description
--header ヘッダーのコメント: "stripe", "ignore" または任意のテキスト

hoistPatternLet

let または var のバインディングをパターン内に再配置する。

option description
--paternlet let/ var のパターン: "hoist"(default) or "inline"

サンプル

- (let foo, let bar) = baz()
+ let (foo, bar) = baz()
- if case .foo(let bar, let baz) = quux {
    // inner foo
  }

+ if case let .foo(bar, baz) = quux {
    // inner foo
  }

indent

スコープレベルでコードをインデントする。

option description
--indent インデントするスペースの数。タブを使用する場合は"tab"
--tabwidth タブ文字の幅。デフォルトは"unspecified"
--smarttabs タブ幅に関係なくコードを揃える。デフォルトは"enabled"
--indentcase switch 内のケースをインデント: "true" or "false"
--ifdef #if インデント: "indent"(default) or "no-indent" or "outdent"
--xcodeindentation Xcode インデント guard/enum: "enabled" or "disabled"(default)

サンプル

if x {
-     // foo
} else {
-     // bar
-       }

if x {
+   // foo
} else {
+   // bar
+ }
let array = [
  foo,
-     bar,
-       baz
-   ]

let array = [
  foo,
+   bar,
+   baz
+ ]
switch foo {
-   case bar: break
-   case baz: break
}

switch foo {
+ case bar: break
+ case baz: break
}

isEmpty

count == 0isEmpty に変換する

※ まれに、isEmpty プロパティが無い型で挿入される可能性があるため。ルールはデフォルトで無効になって
いる。--enable isEmpty を使用して有効にする必要がある。

サンプル

- if foo.count == 0 {
+ if foo.isEmpty {

- if foo.count > 0 {
+ if !foo.isEmpty {

- if foo?.count == 0 {
+ if foo?.isEmpty == true {

leadingDelimiters

先頭のカンマを前の行の末尾に移動する。

サンプル

- guard let foo = maybeFoo // first
-     , let bar = maybeBar else { ... }

+ guard let foo = maybeFoo, // first
+      let bar = maybeBar else { ... }

linebreakAtEndOfFile

ファイルの最後に空行を入れる。

linebreaks

全ての改行に指定された改行文字を使用する。(CR, LF or CRLF)

option description
--linebreaks 使用する改行文字: "cr", "crlf" or "lf"(default)

modifierOrder

修飾子に一貫した順序を使用する。

option description
--modifierorder 修飾子をカンマで区切ったリスト(優先度順)

サンプル

- lazy public weak private(set) var foo: UIView?
+ public private(set) lazy weak var foo: UIView?
- final public override func foo()
+ override public final func foo()
- convenience private init()
+ private convenience init()

numberFormatting

数値リテラルを一貫性のあるグループにする。可読性をあげるためグループは _ 区切り文字で区切られる。
数値タイプごとに、グループサイズ(各グループの桁数)としきい値(グループ化を適用する前の数値の最小桁数)
を指定できる。

option description
--decimalgrouping 10進数のグループ化、しきい値(default: 3, 6) or "none", "ignore"
--binarygrouping バイナリグループ化、しきい値(default: 4, 8) or "none", "ignore"
--octalgrouping 8進数のグループ化、しきい値(default: 4, 8) or "none", "ignore"
--hexgrouping 16進数のグループ化、しきい値(default: 4, 8) or "none", "ignore"
--fractiongrouping .の後の数字をグループ化: "enabled" or "disabled"(default)
--exponentgrouping グループ指数桁数: "enabled" or "disabled"
--hexliteralcase 16進数リテラルのケーシング: "uppercase" (default) or "lowercase"
--exponentcase 数字の e の大文字小文字: "lowercase" or "uppercase" (default)

サンプル

- let color = 0xFF77A5
+ let color = 0xff77a5
- let big = 123456.123
+ let big = 123_456.123

preferKeyPath

簡易的な map { $0.foo } クロージャーを keyPath ベースの構文にする。

サンプル

- let barArray = fooArray.map { $0.bar }

+ let barArray = fooArray.map(\.bar)

redundantBackticks

識別子の周りの冗長なバックティックを削除する。

サンプル

- let `infix` = bar
+ let infix = bar
- func foo(with `default`: Int) {}
+ func foo(with default: Int) {}

redundantBreak

スイッチケースの冗長なブレークケースを削除する。

サンプル

switch foo {
  case bar:
      print("bar")
-       break
  default:
      print("default")
-       break
}

redundantGet

computedProperty 内の不要な get 句を削除する。

サンプル

var foo: Int {
-   get {
-     return 5
-   }
}

var foo: Int {
+   return 5
}

redundantInit

必要なければ明示的に init を削除する。

サンプル

- String.init("text")
+ String("text")

redundantLet

定数名・変数名が必要無い場合は冗長な let/var を削除する。

サンプル

- let _ = foo()
+ _ = foo()

redundantLetError

catch 句から冗長な let error を削除する。

サンプル

- do { ... } catch let error { log(error) }
+ do { ... } catch { log(error) }

redundantNilInit

オプショナルの場合に不必要な nil 宣言を削除する。

サンプル

- var foo: Int? = nil
+ var foo: Int?

redundantObjc

不必要な @objc アノテーションを削除する。

サンプル

- @objc @IBOutlet var label: UILabel!
+ @IBOutlet var label: UILabel!
- @IBAction @objc func goBack() {}
+ @IBAction func goBack() {}
- @objc @NSManaged private var foo: String?
+ @NSManaged private var foo: String?

redundantParens

サンプル

- if (foo == true) {}
+ if foo == true {}
- while (i < bar.count) {}
+ while i < bar.count {}
- queue.async() { ... }
+ queue.async { ... }
- let foo: Int = ({ ... })()
+ let foo: Int = { ... }()

redundantPattern

不必要なパターンマッチングパラメータ構文を削除する。

サンプル

- if case .foo(_, _) = bar {}
+ if case .foo = bar {}
- let (_, _) = bar
+ let _ = bar

redundantRawValues

enum の不必要な raw string value を削除する。

サンプル

enum Foo: String {
-   case bar = "bar"
  case baz = "quux"
}

enum Foo: String {
+   case bar
  case baz = "quux"
}

redundantReturn

不必要な return キーワードを削除する。

サンプル

- array.filter { return $0.foo == bar }
+ array.filter { $0.foo == bar }

redundantSelf

self を挿入または削除する。

@autoclosure
引数を持つ関数では、呼び出しで self が必要になる場合があるが、SwiftFormat
はこれを自動的に検出できないので注意が必要。もし、@autoclosure 引数を持つ関数がある場合には、
--selfrequired で関数を指定することで redundantSelf ルールを無視することができる。

option description
--self self の明示的な設定: "insert" or "remove"(default) or "init-only"
--selfrequired @autoclosure 引数を持つ関数のカンマ区切りリスト

サンプル

func foobar(foo: Int, bar: Int) {
  self.foo = foo
  self.bar = bar
-   self.baz = 42
}

func foobar(foo: Int, bar: Int) {
  self.foo = foo
  self.bar = bar
+   baz = 42
}

--self オプションの init-onlyinit内でのみ有効。

init(foo: Int, bar: Int) {
  self.foo = foo
  self.bar = bar
-   baz = 42
}

init(foo: Int, bar: Int) {
  self.foo = foo
  self.bar = bar
+   self.baz = 42
}

redundantVoidReturnType

不必要な void の return を削除する。

サンプル

- func foo() -> Void {
    // returns nothing
  }

+ func foo() {
    // returns nothing
  }

semicolons

セミコロンの削除。なお、コードの動作に影響があるようなセミコロンは削除されない。

option description
--semicolons セミコロンの許可: "never" or "inline"(default)

サンプル

- let foo = 5;
+ let foo = 5
- let foo = 5; let bar = 6
+ let foo = 5
+ let bar = 6

spaceAroundBraces

中括弧周りのスペースを追加または削除する。

サンプル

- foo.filter{ return true }.map{ $0 }
+ foo.filter { return true }.map { $0 }
- foo( {} )
+ foo({})

spaceAroundBrackets

角括弧周りのスペースを追加または削除する。

サンプル

- foo as[String]
+ foo as [String]
- foo = bar [5]
+ foo = bar[5]

spaceAroundComments

コメントの前後にスペースを追加する。

サンプル

- let a = 5// assignment
+ let a = 5 // assignment
- func foo() {/* ... */}
+ func foo() { /* ... */ }

spaceAroundGenerics

山括弧周りのスペースを削除する。

サンプル

- Foo <Bar> ()
+ Foo<Bar>()

spaceAroundOperators

演算子・区切り文字周りのスペースを追加または削除する。

option description
--operatorfunc オペレータ関数のスペース間隔: "spaced"(default) or "no-space"
--nospaceoperators スペースを含まない演算子のリスト

サンプル

- foo . bar()
+ foo.bar()
- a+b+c
+ a + b + c
- func ==(lhs: Int, rhs: Int) -> Bool
+ func == (lhs: Int, rhs: Int) -> Bool

spaceAroundParens

括弧の前後のスペースを追加または削除する。

サンプル

- init (foo)
+ init(foo)
- switch(x){
+ switch (x) {

spaceInsideBraces

中括弧の内側にスペースを追加する。

サンプル

- foo.filter {return true}
+ foo.filter { return true }

spaceInsideBrackets

角括弧内のスペースを削除する。

サンプル

- [ 1, 2, 3 ]
+ [1, 2, 3]

spaceInsideComments

コメント内に先頭と末尾のスペースを追加する。

サンプル

- let a = 5 //assignment
+ let a = 5 // assignment
- func foo() { /*...*/ }
+ func foo() { /* ... */ }

spaceInsideGenerics

山括弧内のスペースを削除する。

サンプル

- Foo< Bar, Baz >
+ Foo<Bar, Baz>

spaceInsideParens

括弧内のスペースを削除する。

サンプル

- ( a, b)
+ (a, b)

strongOutlets

@IBOutlet から weak 修飾子を削除する。Apple の推奨

サンプル

- @IBOutlet weak var label: UILabel!
+ @IBOutlet var label: UILabel!

strongifiedSelf

optional の unwrap 式で、self の周りのバックティックを削除する。

※ エスケープされていない self への割り当ては Swift4.2以降でのみサポートされているため、Swift
のバージョンが 4.2以降に設定されていない場合はこのルールは無効になる。

サンプル

- guard let `self` = self else { return }
+ guard let self = self else { return }

todos

TODO:, MARK:, FIXME: に正しいフォーマットを適用する。

サンプル

- /* TODO fix this properly */
+ /* TODO: fix this properly */
- // MARK - UIScrollViewDelegate
+ // MARK: - UIScrollViewDelegate

trailingClosures

該当する場合は、末尾の閉鎖構文を使用する。

option description
--trailingclosures 末尾のクロージャを使用するカンマ区切りのリスト

サンプル

- DispatchQueue.main.async(execute: { ... })
+ DispatchQueue.main.async {
- let foo = bar.map({ ... }).joined()
+ let foo = bar.map { ... }.joined()

trailingCommas

コレクションリテラルの最後の項目の末尾のコンマを追加または削除する。

option description
--commas コレクションリテラル内のカンマ: "always"(default) or "inline"

サンプル

let array = [
  foo,
  bar,
-   baz
]

let array = [
  foo,
  bar,
+   baz,
]

trailingSpace

行末の末尾のスペースを削除する。

option description
--trimwhitespace 行末のスペーストリム: "always"(default) : "nonblank-lines"

typeSugar

Array, Dictionary, Optional の短縮構文を優先する。

option description
--shortoptionals Optionalを使用する: "always"(default) : "except-properties"

サンプル

- var foo: Array<String>
+ var foo: [String]
- var foo: Dictionary<String, Int>
+ var foo: [String: Int]
- var foo: Optional<(Int) -> Void>
+ var foo: ((Int) -> Void)?

unusedArguments

関数で使用していない引数を _ でマークする。

option description
--stripunusedargs "closure-only", "unnamed-only" or "always" (default)

サンプル

- func foo(bar: Int, baz: String) {
    print("Hello \(baz)")
  }

+ func foo(bar _: Int, baz: String) {
    print("Hello \(baz)")
  }
- func foo(_ bar: Int) {
    ...
  }

+ func foo(_: Int) {
    ...
  }
- request { response, data in
    self.data += data
  }

+ request { _, data in
    self.data += data
  }

void

型の宣言には Void を使用する。

option description
--voidtype Void タイプの表現方法: "void"(default) : "tuple"

サンプル

- let foo: () -> ()
+ let foo: () -> Void
- let bar: Void -> Void
+ let bar: () -> Void
- let baz: (Void) -> Void
+ let baz: () -> Void
- func quux() -> (Void)
+ func quux() -> Void

wrap

指定された最大幅を超える行を折り返す。

option description
--maxwidth 折り返す行の最大長: "none"(default)
--nowrapoperators ラップされるべきでない演算子のカンマ区切りのリスト

wrapArguments

ラップされた関数の引数またはコレクションの要素を揃える。

※ 以前のバージョンとの互換性の関係で --wrapparameters に値が指定されていない場合は、--wraparguments
の値が使用される。

option description
--wraparguments 全ての引数をラップする: "before-list", "after-list", "preserve"
--wrapparameters 関数のパラメーターをラップする: "before-first", "after-first", "preserve"
--wrapcollections 配列・辞書をラップする: "before-first", "after-first", "preserve"
--closingparen 閉じ括弧の位置: "balanced" (default) or "same-line"

サンプル

--wraparguments before-first

- foo(bar: Int,
-     baz: String)

+ foo(
+   bar: Int,
+   baz: String
+ )
- class Foo<Bar,
-           Baz>

+ class Foo<
+   Bar,
+   Baz
+ >

--wrapparameters after-first

- func foo(
-   bar: Int,
-   baz: String
- ) {
    ...
  }

+ func foo(bar: Int,
+          baz: String) {
    ...
  }

--wrapcollections before-first

- let foo = [bar,
             baz,
-            quuz]

+ let foo = [
+   bar,
    baz,
+   quuz
+ ]

wrapAttributes

@attributes の折り返しを設定する。

option description
--funcattributes 関数について: "preserve", "prev-line", or "same-line"
--typeattributes タイプについて: "preserve", "prev-line", or "same-line"

サンプル

--funcattributes prev-line

- @objc func foo() {}

+ @objc
+ func foo() { }

--funcattributes same-line

- @objc
- func foo() { }

+ @objc func foo() {}

--typeattributes prev-line

- @objc class Foo {}

+ @objc
+ class Foo { }

--typeattributes same-line

- @objc
- enum Foo { }

+ @objc enum Foo {}

wrapMultilineStatementBraces

複数行のステートメントの開始括弧の位置を一段下げる。(if / guard / while / func)

サンプル

if foo,
-   bar {
  // ...
}

if foo,
+   bar
+ {
  // ...
}
guard foo,
-   bar else {
  // ...
}

guard foo,
+   bar else
+ {
  // ...
}
func foo(
  bar: Int,
-   baz: Int) {
  // ...
}

func foo(
  bar: Int,
+   baz: Int)
+ {
  // ...
}

yodaConditions

式の右側にある定数値を優先する。

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

複数のボタンを、1つの変数でまとめる。

オブジェクト
(Label とか Buttonとか Text Field とかもオブジェクト。)
(Swiftは、オブジェクト指向言語。)
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

複数のオブジェクトを、1つの変数でまとめる。

こんな感じで、複数のボタンを並べたいとき、
スクリーンショット 2020-08-05 21.27.58.png

var Button1, var Button2, var Button3, var Button4,でも良いのですが、
(@IBOutlet weak var Button1: UIButton! ✖️ 4)

    @IBOutlet weak var Button1: UIButton!
    @IBOutlet weak var Button2: UIButton!
    @IBOutlet weak var Button3: UIButton!
    @IBOutlet weak var Button4: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        Button1.layer.cornerRadius = 8
        Button2.layer.cornerRadius = 8
        Button3.layer.cornerRadius = 8
        Button4.layer.cornerRadius = 8
    } 

コードを1つに、まとめる

オブジェクトを1つの変数でまとめたい ときは、以下の通り。

for構文を使うとできるらしいです?
[ ] とかも、使います。
@IBOutlet var Buttons: [UIButton]!

   @IBOutlet var Buttons: [UIButton]!

    override func viewDidLoad() {
        super.viewDidLoad()

       for button in Buttons {          <-------------------------
           button.layer.cornerRadius = 10
        }
    }

注意点?

通常の Outlet ではなく、Outlet Collection を選択します。

まず一つ目の部品を選択し、
コード画面にドラッグしたとき、ポップアップウインドウはこんな感じ。

この状態では一つの部品しか関連付いていないので、
コード左側の ◉ をOutlet Collection として使いたい部品にドラッグし、関連付けます。

この時は、Optionキーを押さなくてもOKです。

まとめ

今宵の満月は綺麗でござる。
1000年前、藤原道長も同じ月を見ていたを思うと、しみじみシジミ。

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

ボタンの角を丸くする?

ボタンの角を丸くする?

Buttonを、中流の河原の石にしたいと思います。
侵食作用を及ぼすコードは、以下の通りです。

スクリーンショット 2020-08-05 21.30.40.png

   @IBOutlet weak var startButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        startButton.layer.cornerRadius = 10.0<--------------

    }

-Outletで接続したのち、
startButton.layer.cornerRadius = 10.0を記述します。(10は、任意の数字)

おしまい。

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

[swift5]エラーNo such module 'ライブラリ名'

エラー状況

画像のようにXcodeでビルドを行うとNo such module 'ライブラリ名'というエラーが発生する。
image.png

対応したこと

①Xcodeの再起動
②Macの再起動
③podfileの確認とpod installを再度実行

①〜③全て不発...
検索を行い色々と模索していると解決しそうな記事に遭遇!
https://qiita.com/doge_kun55/items/26ac2fa2031ddbeee879

解決方法

XcodeのツールバーにあるProduct/Scheme/ManageSchemes/を選択。
すると以下のようにページが開く。
image.png

今回の場合はデフォルトでSwift5Boketeにチェックがついており、その他にはチェックはついていませんでした。
なので全ての項目のshowsharedにチェックをつけてCloseをクリック。

そのあと、再ビルドを実行すると無事ビルドされました。

最後に

ちなみに、今回エラーの解決はしていますが、何が原因で、どういう理由で解決に至ったかということが理解できておりません。もし、この記事を見た方の中に原因をわかる方がおられましたらご教授の程宜しくお願い致します。

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

Buildしたけど真っ黒、アルアル原因まとめ

Buildしたけど真っ黒、アルアル原因まとめ

2点。

1, NSUnknownKeyException

お笑い養成所(NSC)ではなく、NS Unknown〜(※)です。

ViewControllerMain StoryBoardを繋げるとき、 やっぱ繋げるのやめようってなって コードを削除しても、繋がりは切れません

不要な接続が繋がったままだから、エラーが起きるみたい。
NSUnknownKeyExceptionが出た時の回避方法

20130925_1583616.png

ユーティリティーエリアの、Show the Connection Inspectorからも同じ操作できます。
(メニューバーの、一番右のボタン)


 ※NS Unknown〜について。

Objective-C では、Apple開発にてNS Errorクラスを使って、エラーを表すらしい。
その名残かな?(ちょっと分からない。)
ErrorとNSErrorに関するいくつかの実験

てかNSCって、New Star Creationの略なのか?

2, BreakPoint

右クリック、Delete BreakPointsで消せます。

余談。 Xcodeショートカットキーについて。

今日知った、便利なショートカットキー5個。

-Ctrl A
-Ctrl E 後ろ

-Fn + back DELETE
(WindowsでいうDELETEキー。BackSpaceの逆方向)

-Option押しながら、ドラッグ&ドロップ  
矩形選択みたいなこと出来る。Option離したら、解除されちゃう。

-Command + shift + L オブジェクトライブラリ表示
(Option長押し、表示を維持)
(一回ドラッグ&ドロップすれば、あとは長押し続けなくても表示を維持)

スクリーンショット 2020-08-05 18.55.11.png

終わりに

久々に見たけど、
BackNumber「高嶺の花子さん」MV 神。

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

サードパーティ製アプリをiOS標準Webブラウザとして登録するための条件

iOS14では、デフォルトで起動するWebブラウザを「Safari」以外のアプリにユーザー自身が設定できる様になります。
今回「サードパーティ製アプリが標準ブラウザになる為の要件」が公開されたので簡単にまとめてみました。

概要

標準ブラウザになる為の各要件をクリアする

Appleへメール(default-browser-requests@apple.com)で申請

という流れになる様です。

承認後は、Entitlementsに「com.apple.developer.web-browser」キーを追加する形に?

標準ブラウザになったら出来ること

Be an option for the user to choose as their default browser.

  • 「標準で開くブラウザ」としてユーザが選択出来る様になる

Load pages from all domains with full script access.

  • フルスクリプトアクセスですべてのドメインからページをロード可能に。

Use Service Workers in WKWebView instances.

  • WKWebViewでService Workersが使用出来る

必須要件

Your app must specify the HTTP and HTTPS schemes in its Info.plist file.

  • HTTP/HTTPSのURLスキームをinfo.plistに設定している

Your app can’t use UIWebView.

  • UIWebView(既にDeprecated)を使用していない

On launch, the app must provide a text field for entering a URL, search tools for finding relevant links on the internet, or curated lists of bookmarks.

  • 「URL入力欄」、「検索窓」または「整理済みブックマーク」を起動時に提示している

The app must navigate directly to the specified destination and render the expected web content. Apps that redirect to unexpected locations or render content not specified in the destination’s source code don’t meet the requirements of a default web browser.

  • HTTP/HTTPSのURLを開く場合、ユーザが指定したページに直接移動して、予期されたWebコンテンツをレンダリングしている

→ ユーザが予期しないページへのリダイレクトや、指定されていないコンテンツをレンダリングするアプリはNG。
ただし、以下は例外

Apps designed to operate in a parental controls or locked down mode may restrict navigation to comply with those goals.

  • 「ペアレンタルやロックダウンモード機能」があるアプリは、ナビゲーションを制限してこれらの目標に準拠する場合がある。

Your app may present a “Safe Browsing” or other warning for content suspected of phishing or other problems.

  • フィッシングや詐欺などの問題が疑われるコンテンツに対しては、セーフブラウジング機能が働いたり、警告を表示する場合がある。

Your app may offer a native authentication UI for a site that also offers a native web sign-in flow.

  • ネイティブWebサインインフローも提供するサイトにネイティブ認証UIを提供する場合がある。

こちらの記事: https://iphone-mania.jp/news-304786/ では

  • ペアレンタルコントロール機能もしくはロックダウンモードを持つこと
  • セーフブラウジング、もしくはフィッシングなどの問題への警告を表示すること

も要件としていましたが、これについてはこちらのツイート


で指摘されている通り
「ペアレンタルやロックダウンモード機能」と「セーフブラウジング、フィッシングへの警告表示」は必須項目ではないと思います。

標準ブラウザ資格を持ったアプリに対する制限

標準ブラウザとなった場合にはユーザのWeb閲覧を独占するため、個人データへの不要なアクセスは回避すべきとの方針。

Info.plistで以下のキーを持っているとリジェクト

  • NSPhotoLibraryUsageDescription
     → 写真を取得する際はPHPickerViewControllerを使用する。「NSPhotoLibraryAddUsageDescription」の使用はOK。

  • NSLocationAlwaysUsageDescription、NSLocationAlwaysAndWhenInUseUsageDescription
     → 代わりに「NSLocationWhenInUseUsageDescription」を使用する

  • NSHomeKitUsageDescription
     → HomeKitへのアクセス禁止

  • NSBluetoothAlwaysUsageDescription
     → 代わりに「NSBluetoothWhileInUseUsageDescription」を使用

  • NSHealthShareUsageDescription、NSHealthUpdateUsageDescription
     → ヘルスデータへのアクセス禁止

参考

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

MarketingCloudSDK iOSでリッチPush通知を送信する

環境

・xcode Version 11.3.1 (11C504)
・swift Version 5.1.3
・MarketingCloudSDK iOS (v7.2.1)
・Mac OS 10.14.6(Mojave)

準備

MarketingCloudSDK iOSでシンプルなpush通知を送信する
の続きとなります。

リッチPushとは

push通知を開くと遷移したいurlに飛べたり、push通知に画像、動画などを表示することができる。

リッチPush通知の実装

1.opendirect機能(ディープリンクとも呼ばれる)

AppDelegate.swift
MarketingCloudSDK.sharedInstance().sfmc_setURLHandlingDelegate(self)

上記コードをSDKの初期設定記述の後に、AppDelegate.swift内に記述する。
その後、
MarketingCloudSDKURLHandlingDelegateプロトコルメソッドを追加

AppDelegate.swift
extension AppDelegate: MarketingCloudSDKURLHandlingDelegate {
    func sfmc_handle(_ url: URL, type: String) {
        print(url)
        if UIApplication.shared.canOpenURL(url) == true {
            // ios 10.0以上
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:], completionHandler: { success in
                    if success {
                        print("url \(url) opened successfully")
                    } else {
                        print("url \(url) could not be opened")
                    }
                })
            } else {
                if UIApplication.shared.openURL(url) == true {
                    print("url \(url) opened successfully")
                } else {
                    print("url \(url) could not be opened")
                }
            }
        }
    }
}

※指定できるのは「:」を含むURL形式の値のみ

2.push通知に画像、動画などを表示する

xcode上でNotification Service Extensionを作成する

File → New → target...を選択する
スクリーンショット 2020-08-05 11.43.10.png
Notification Service Extensionを選択してNextをクリックする
スクリーンショット_2020_08_05_11_45.png
productnameを入力してfinishボタンをクリックする
スクリーンショット 2020-08-05 11.49.39.png
上記のようなポップアップがでてきたらActivateをクリックする

Notification Service Extensionの実装
Notification Service Extensionのフォルダができるので、その中のNotificationService.swiftに実装をしていく

実装イメージは下記となります

AppDelegate.swift
/*
 * Copyright (c) 2017, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root  or https://opensource.org/licenses/BSD-3-Clause
 */

import CoreGraphics
import UserNotifications

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((_ contentToDeliver: UNNotificationContent) -> Void)? = nil
    var modifiedNotificationContent: UNMutableNotificationContent?

    func createMediaAttachment(_ localMediaUrl: URL) -> UNNotificationAttachment {
        // options: specify what cropping rectangle of the media to use for a thumbnail
        //          whether the thumbnail is hidden or not
        let clippingRect = CGRect.zero
        let mediaAttachment = try? UNNotificationAttachment(identifier: "attachmentIdentifier", url: localMediaUrl, options: [UNNotificationAttachmentOptionsThumbnailClippingRectKey: clippingRect.dictionaryRepresentation, UNNotificationAttachmentOptionsThumbnailHiddenKey: false])
        return mediaAttachment!
    }

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {

        // save the completion handler we will call back later
        self.contentHandler = contentHandler

        // make a copy of the notification so we can change it
        modifiedNotificationContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        // does the payload contains a remote URL to download or a local URL?
        if let mediaUrlString = request.content.userInfo["_mediaUrl"] as? String {
            // see if the media URL is for a local file  (i.e., file://movie.mp4)
            guard let mediaUrl = URL(string: mediaUrlString) else {
                // attempt to create a URL to a file in local storage
                var useAlternateText: Bool = true
                if mediaUrlString.isEmpty == false {
                    let mediaUrlFilename:NSString = mediaUrlString as NSString
                    let fileName = (mediaUrlFilename.lastPathComponent as NSString).deletingPathExtension
                    let fileExtension = (mediaUrlFilename.lastPathComponent as NSString).pathExtension

                    // is it in the bundle?
                    if let localMediaUrlPath = Bundle.main.path(forResource: fileName, ofType: fileExtension) {
                        // is the URL a local file URL?
                        let localMediaUrl = URL.init(fileURLWithPath: localMediaUrlPath)
                        if localMediaUrl.isFileURL == true {
                            // create an attachment with the local media
                            let mediaAttachment: UNNotificationAttachment? = createMediaAttachment(localMediaUrl)

                            // if no problems creating the attachment, we can use it
                            if mediaAttachment != nil {
                                // set the media to display in the notification
                                modifiedNotificationContent?.attachments = [mediaAttachment!]

                                // everything is ok
                                useAlternateText = false
                            }
                        }
                    }
                }

                // if any problems creating the attachment, use the alternate text if provided
                if (useAlternateText == true) {
                    if let mediaAltText = request.content.userInfo["_mediaAlt"] as? String {
                        if mediaAltText.isEmpty == false {
                            modifiedNotificationContent?.body = mediaAltText
                        }
                    }
                }

                // tell the OS we are done and here is the new content
                contentHandler(modifiedNotificationContent!)
                return
            }

            // if we have a URL, try to download media (i.e., https://media.giphy.com/media/3oz8xJBbCpzG9byZmU/giphy.gif)
            if mediaUrl.isFileURL == false {
                // create a session to handle downloading of the URL
                let session = URLSession(configuration: URLSessionConfiguration.default)

                // start a download task to handle the download of the media
                weak var weakSelf: NotificationService? = self
                session.downloadTask(with: mediaUrl, completionHandler: {(_ location: URL?, _ response: URLResponse?, _ error: Error?) -> Void in
                    var useAlternateText: Bool = true
                    // if the download succeeded, save it locally and then make an attachment
                    if error == nil {
                        let downloadResponse = response as! HTTPURLResponse
                        if (downloadResponse.statusCode >= 200 && downloadResponse.statusCode <= 299) {
                            // download was successful, attempt save the media file
                            let localMediaUrl = URL.init(fileURLWithPath: location!.path + mediaUrl.lastPathComponent)

                            // remove any existing file with the same name
                            try? FileManager.default.removeItem(at: localMediaUrl)

                            // move the downloaded file from the temporary location to a new file
                            if ((try? FileManager.default.moveItem(at: location!, to: localMediaUrl)) != nil) {
                                // create an attachment with the new file
                                let mediaAttachment: UNNotificationAttachment? = weakSelf?.createMediaAttachment(localMediaUrl)

                                // if no problems creating the attachment, we can use it
                                if mediaAttachment != nil {
                                    // set the media to display in the notification
                                    weakSelf?.modifiedNotificationContent?.attachments = [mediaAttachment!]

                                    // everything is ok
                                    useAlternateText = false
                                }
                            }
                        }
                    }

                    // なにか問題が起こり、代替テキストを設定している場合は代替テキストを表示
                    if (useAlternateText == true) {
                        if let mediaAltText = request.content.userInfo["_mediaAlt"] as? String {
                            if mediaAltText.isEmpty == false {
                                weakSelf?.modifiedNotificationContent?.body = mediaAltText
                            }
                        }
                    }

                    // tell the OS we are done and here is the new content
                    weakSelf?.contentHandler!((weakSelf?.modifiedNotificationContent)!)
                }).resume()
            }
        }
        else {
            // no media URL found in the payload, just pass on the orginal payload
            contentHandler(request.content)
            return
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        // we took too long to download the media URL, use the alternate text if provided
        if let mediaAltText = modifiedNotificationContent?.userInfo["_mediaAlt"] as? String {
            // alternative text to display if there are any issues loading the media URL
            if mediaAltText.isEmpty == false {
                modifiedNotificationContent?.body = mediaAltText
            }
        }

        // 取得に失敗したときの処理
        contentHandler!(modifiedNotificationContent!)
    }
}

※動画はmp4に対応

SMCからリッチPush通知を送信する

無事とどけばOK

参考リンク(公式)

https://salesforce-marketingcloud.github.io/MarketingCloudSDK-iOS/sdk-implementation/implementation-urlhandling.html
https://salesforce-marketingcloud.github.io/MarketingCloudSDK-iOS/push-notifications/rich-notifications.html

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

tableviewのサイズを中身のサイズに合わせる。

tableviewを設定画面に利用するとき、数個しか要素がないのにtableviewのサイズが大きいままで気持ち良くなかった。

override func viewWillLayoutSubviews() {
    table.frame.size = table.contentSize
}

サイズを中身の大きさに合わせて変更してやればいい感じになった。

わーい!

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

JavaScriptからletを「絶滅」させるために足りないもの

"JavaScriptからletを絶滅させ、constのみにするためのレシピ集" という投稿を読みました。半分はネタだと思いますが、 JavaScript で const を追求すると可読性が厳しそう だなと感じました。

一方で、他の言語だと同じことにチャレンジしても、もう少しマシに書けそうに思いました。僕が普段一番よく使っている言語は Swift です。そこで、試しに Swift で同じ内容のコードを書いてみて、 JavaScript で let を「絶滅」させるために足りないもの が何かを考えてみました。

なお、 JavaScript のコードは注釈がない限り上記の投稿からの引用です。

変数・定数宣言のためのキーワード

変数 定数
JavaScript let const
Swift var let

JavaScirpt と Swift では変数・定数宣言のためのキーワードが異なります。同じ let というキーワードが JavaScript では変数宣言に、 Swift では定数宣言に用いられていてややこしいです。そのため、本投稿ではそれぞれ「 let (変数)」・「 let (定数)」として区別します。

初級

10回繰り返したいfor文

// JavaScript ( Lodash を利用)
_.range(10).forEach(i => {
  console.log(i)
})

これを Swift で書くと次のようになります。

// Swift
for i in 0 ..< 10 {
    print(i)
}

let (定数)が省略されていますが、 ilet (定数)で宣言されます。 0 ..< 10 は標準ライブラリの Range を生成します。

元記事の JavaScript のコードと同じように forEach を使って書くこともできます。しかし、 forEachbreakcontinue, return などと相性が良くないので、 for 文などの制御構文が使える場合は、制御構文を優先した方が良いと思います。

なお、 JavaScript でも for...of 文を使えば次のように書けます。

// JavaScript (※引用でない、 Lodash を利用)
for (const i of _.range(10)) {
    console.log(i);
}

元記事の想定環境は ES2017 ということですが、 for...of は ES2015 で導入されているので上記で良いように思います。ただ、僕は普段 JavaScript をバリバリ書いているわけではないので、 ES2017 前提で for...of より forEach を優先した方が良い理由があれば教えてもらえるとうれしいです。

数値配列の合計値を算出

// JavaScript( Lodash を利用)
const arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
const sum = _.sum(arr)
// JavaScript (reduceが分かる人向け)
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0)

Swift の + は関数なので、次のようにして reduce+ を渡して簡潔に書けます。

// Swift
let sum = arr.reduce(0, +)

また、標準ライブラリに sum はないですが、次のように extension を書けばメソッドを追加できます。

// Swift
extension Sequence where Element: AdditiveArithmetic {
    func sum() -> Element {
        reduce(.zero, +)
    }
}
// Swift
let sum = arr.sum()

この sum メソッドは Array が準拠する Sequence に対して追加しているので、 Array の他に SetRange など任意の Sequence に対して利用できます。

// Swift
let sum = (1 ... 10).sum() // 55

なお、 Swift では標準ライブラリの型に独自のメソッドを追加しても、 JavaScript と比べると衝突の心配は小さいです。 Swift では同名のメソッドを追加した複数のライブラリを同時に利用しても、それぞれ異なるシンボル名にコンパイルされているので干渉し合うことはありません。同一ファイル上でそれらを複数 import した場合はコード上でどれを指しているか曖昧になるのでコンパイルエラーになりますが、それは別名を付けるなどして回避可能です。1

JavaScript でも prototype にメソッドを追加することはできますが、 Array などの標準の型への追加は推奨されません。 let (変数)を「絶滅」させるには、 let (変数)を書きたくなるようなケースで気軽に extension でカバーできると良いように思います。 let (変数)の利用を extension 側に閉じ込められれば、ユーザーコードから let (変数)を「絶滅」させる助けになるでしょう。

足りない機能を関数として提供することはできますが、標準ではメソッドを使うことが多いので、スタイルが混ざるのはコードを書く上でも読む上でも望ましくありません。たとえば、 ES2019 で flatMap が追加されましたが、 flatMap が関数ではなくメソッドとして提供されるのはうれしいのではないでしょうか。

JavaScript はその仕組み上、標準の型を拡張しながら衝突を避けるのは難しいので、標準で十分な道具(まさに flatMap のような)が提供されることが let の「絶滅」に役立つのではないかと思います。

オブジェクトの配列の合計値を算出

// JavaScript( Lodash を利用)
const users = [{ name: 'person1', age: 10 }, { name: 'person2', age: 20 }, { name: 'person3', age: 30 }]
const sumOfAge = _.sumBy(users, 'age')
console.log(sumOfAge)
// JavaScript (reduceが分かる人向け)
const sumOfAge = users.reduce((accumulator, currentUser) => accumulator + currentUser.age, 0)

reduce だけで書くと、 User から age を取り出すコードと、それらの合計を計算するコードが一体化して可読性が下がります。 map して ageArray に変換してから合計した方が読みやすいでしょう。しかし、その場合、 map は中間計算のためだけに Array インスタンスを生成することになり無駄です(何百万個の要素を持つ巨大な Array かもしれません)。 LazySequence を使えばそのような問題を解決できます。

// Swift
let sumOfAge = users.lazy.map(\.age).reduce(0, +)

LazySequencemap による変換は即時実行されません。上記コードでは reduce 時に遅延して要素を取り出し、要素ごとに map の変換処理が行われます。そのため、中間計算のために巨大な実体を持ったコレクションが生成されることを防げます。 JavaScript でも、これに相当する手段を標準で提供してくれれば、パフォーマンスを犠牲にせず可読性を向上させることができるでしょう。

またこの例のように、 UserArrayageArray に変換したいようなケースは多いと思います。 Swift の KeyPath\.age )はそのようなケースで役立ちます。 Lodash を使った一つ目の例では 'age' を文字列として渡して似たようなことをしていますが、 \.age は文字列ではなく静的型検査可能なのでより安全です(そもそも JavaScript は動的型付けですが)。

if文

// JavaScript
const tax = isTakeout ? 0.08 : 0.1

Swift でも同様に三項演算子が使えます。

// Swift
let tax = isTakeout ? 0.08 : 0.1

しかし、 Swift では if 文で分岐しても let (定数)が使えます。

// Swift
let tax: Double
if isTakeout {
  tax = 0.08
} else {
  tax = 0.1
}

もし else が存在しないなど、定数が網羅的に初期化されないとコンパイルエラーになります。

この例では三項演算子が適切でしょうが、これができることが後で大きな差となって利いてきます。

じゃあswitch文どうするのよ

// JavaScript
const getMessageByStatus = (status) => {
  switch (status) {
  case 200:
    return 'OK'
  case 204:
    return 'No Content'
  // ...省略
  }
}

const message = getMessageByStatus(response.status)

前述のように、 Swift では制御構文による分岐と let (定数)の初期化を組み合わせられるので次のように書けます。わざわざ switch 一つのために関数を作る必要はありません。

// Swift
let message: String
switch (response.status) {
case 200:
    message = "OK"
case 204:
    message = "No Content"
// ...省略
}
print(message)

中~上級

try-catchとの兼ね合い

元記事では、 const徹底しない JavaScript のコードは次のようになっていました。

// JavaScript
let response
try {
  response = await requestWeatherForecast() // 天気予報APIを叩く
} catch (err) {
  console.error(err)
  response = '曇り' // APIから取得できなかった場合は適当に曇りとか言ってごまかす
}
console.log(response)

しかし、 let (変数)を「絶滅」させるために、上記コードが次のように改変されていました。

// JavaScript
const response = await requestWeatherForecast().catch(err => {
  console.log(err)
  return '曇り'
})

せっかく try/catchasync/await を組み合わせられる仕組みを言語が提供しているにも関わらず、 const を使うために半分 Promise に戻ってしまいました。これはかなり辛いです。

残念ながら Swift には Swift 5.2 現在で async/await がありませんが、 Swift 6 (おそらく 2021 年リリースの次期メジャーバージョン)で導入されそうです 2 3 4 。これが使えると仮定すると、次のように書くことができます。

// Swift
let response: String
do {
    response = try await requestWeatherForecast()
} catch {
    print(error)
    response = "曇り" // APIから取得できなかった場合は適当に曇りとか言ってごまかす
}
print(response)

let (定数)を使っているにも関わらず、 JavaScript の let (変数)を使ったコードとほぼ同じになりました。ここでも制御構文と let (定数)を組み合わせて、初期化を遅延させられることが利いています。 ifswitch による分岐だけでなく、 try/catch による分岐も考慮して、定数の初期化の網羅性が判定されているわけです。

例外catchしたら早期returnしたいんだが

// JavaScript
const shouldReturn = Symbol('shouldReturn') // 普通に文字列の'shouldReturn'でも良いか?
const response = await requestWeatherForecast().catch(err => {
  console.error(err)
  return shouldReturn
})
if (response === shouldReturn) return
console.log(response)

これはさらに辛いです。 let (変数)をなくすために shouldReturn を導入するのは明らかにやりすぎでしょう(これは完全にネタでしょうが)。

これも、 let (定数)と制御構文を組み合わせられれば素直に書くことができます。

// Swift
let response: String
do {
    response = try await requestWeatherForecast() // 天気予報APIを叩く
} catch {
    print(error)
    return
}
print(response)

この場合、エラーケースは早期リターンするので、 requestWeatherForecast が成功した場合だけ response が初期化されるコードになっていても、網羅的に定数が初期化されると判断されます。

リトライ処理

「リトライ処理」について、元記事の const徹底しない コードは↓でした。

// 天気予報APIを叩く。エラーが出たら10回までリトライする
const MAX_RETRY_COUNT = 10
let retryCount = 0
let response
while(retryCount <= MAX_RETRY_COUNT) {
  try {
    response = await requestWeatherForecast() // 天気予報APIを叩く
    break
  } catch (err) {
    console.error(err)
    retryCount++
  }
}
console.log(response)

これを、 let (変数)を「絶滅」させるために、再帰関数を使って↓のように書き換えていました。

// JavaScript
// 与えられた関数をmaxRetryCount回までリトライする関数。
const retryer = (maxRetryCount, fn, retryCount = 0) => {
  if (retryCount >= maxRetryCount) return undefined

  return fn().catch(() => retryer(maxRetryCount, fn, retryCount + 1)) // retryCountを1増やして再帰呼び出し
}

const response = await retryer(MAX_RETRY_COUNT, requestWeatherForecast)

これについては、 Swift で書く場合も var を使わないのは少し難しそうです。 let (定数)と制御構文を組み合わせて定数の初期化を遅延させられるからといって、ループとの組み合わせにはループが一度も実行されないかもしれない難しさがあります。また、ループの中で初期化が必ず一度だけ行われるかをコンパイラが判断するのが困難です。 Swift コンパイラはそこまでやってくれません。

どうしても、 let (定数)で書きたいということであれば、たとえば次のように書くことはできるでしょう。

// Swift
let response: String?
do {
  response = try await (0 ..< maxRetryCount).reduce(nil) { (result, _) in
    if result != nil { return result }
    return try? await requestWeatherForecast()
  }
} catch {
  response = nil
}
print(response)

ただ、こういうケースでは可読性のために var を使って書いた方が良いです。

// Swift
var response: String?
for _ in 0 ..< maxRetryCount {
  do {
    response = try requestWeatherForecast()
    break
  } catch {}
}
print(response)

もしくは、リトライを宣言的に書けるようなライブラリ( Rx とか( Swift なら) Combine とか)を使いましょう。

番外編(不変じゃないconst)

constは再代入できないだけで、constで宣言した配列に要素を追加したり、constで宣言したオブジェクトにプロパティを追加することはできてしまいます。

これらの行為はconstという唯一神をletと同じ地位まで貶める愚行です。

// JavaScript
const arr = []
arr.push(1) // arr: [1]
const obj = {}
obj.a = 1 // obj: { a: 1 }

Swift では let (定数)を使って宣言された Array 型変数に対して変更を加えることはできません。

// Swift
let arr = []
arr.append(1) // ⛔ コンパイルエラー

Swift の Array は値型なので varlet かを変更するだけでミュータビリティをコントロールすることができます。また、 Value Semantics を持つように実装されているので、次のようなコードを書いても定数に格納された Array インスタンスが変更されることはありません。

// Swift
let a = [2, 3, 5]
var b = a
b.append(7) // a は変更されない

print(a) // [2, 3, 5]
print(b) // [2, 3, 5, 7]

配列から条件に合うものだけ抜き出す

// JavaScript
const result = arr.filter(n => n % 2 === 0)

このコードには、特に可読性に関する辛さはないと思います。

Swift でも同様に filter を使って書きます。

// Swift
let result = arr.filter { $0.isMultiple(of: 2) }

% 2 を使っても良いですが、専用のメソッド( isMultiple(of:) )があるのでそれを使った方が良いでしょう。

変数がundefinedじゃないときだけオブジェクトに追加

// JavaScript
const header = {
  'Content-Type': 'application/json',
  ...(token === undefined ? {} : { Authorization: `Bearer ${token}` })
}

これはあえて 定数にこだわるところではない気がしますが、次のように書くことはできます。

// Swift
let header = ["Content-Type": "application/json"]
    .merging(token.map { ["Authorization": "Bearer \($0)"] } ?? [:]) { _, new in new }

ただ、可読性を考えると var を使って次のように書いた方が良いでしょう。

// Swift
var header = ["Content-Type": "application/json"]
if let token = token {
    header["Authorization"] = "Bearer \($0)"
}

とはいえ、リテラルの中で分岐できる処理がほしくなることもあります。 Function Builder を使えば次のようなことができる extension を作ることも可能です5

// Swift
let header: [String: String] = .init {
    ["Content-Type": "application/json"]
    if let token = token {
        ["Authorization": "Bearer \($0)"]
    }
}

オブジェクトの値部分に処理を加える

// JavaScript ( Lodash を利用)
const obj = { a: '1', b: '2', c: '3', d: '4', /* ... */ }
_(obj).mapValues(Number).pickBy(isEven) // { b: 2, d: 4, ... }

Swift には動的なオブジェクトはないので Dictionary で書きます( isEven は Swift の標準ライブラリにないですが、 JavaScript にもないので、別途宣言されているものとします)。

// Swift
let obj = ["a": "1", "b": "2", "c": "3", "d": "4", /* ... */ ]
obj.compactMapValues(Int.init).filter { isEven($0.value) }

キーになるのは compactMapValues です。 StringInt に変換する処理は、 Int("42") ような場合は成功しますが Int("ABC") のような場合には失敗します。 Int.init は失敗したときに nil を返しますが、 compactMapValuesmapValues した上で結果が nil になるエントリーを取り除いてくれます。

JavaScript から let を「絶滅」させるために足りないもの

こうして色々なケースを比較して眺めてみると、 JavaScript から let (変数)を「絶滅」させる一番のハードルは、 const と制御構文の相性の悪さではないでしょうか。特に、 try/catchasync/awaitifswitch と組み合わせて使おうとすると途端に辛くなってしまいます。

もし JavaScript に改変を加えられるとして、この問題を解決するために僕がすぐに思いつく選択肢は次の二つです。

  1. Swift のように、制御フローを解析して網羅的に初期化されている場合は const の初期化を遅延させられるようにする。
  2. Scala や Kotlin のように、 ifswitch 等を式にする。

1 の例は↓です。

// JavaScript (※引用でない)
const a;
if (Math.random() < 0.5) {
    a = 2;
} else {
    a = 3;
}

2 の例は↓です。

// JavaScript (※引用でない)
const a = if (Math.random() < 0.5) {
    2;
} else {
    3;
}

もちろん、これくらいなら三項演算子で書けますが、 try/catchasync/await などと組み合わせてもこれができることが求められます。

他にも、上記で比較してみた範囲でも、

  • 演算子が関数でない
  • mapreduce を遅延させられない(ので中間計算のために実体を伴う無駄なコレクションを生成しないといけない)
  • 標準で足りない道具が多いわりに extension を気軽に書くのが憚られる

などが挙げられました。

逆に言えば、それらが実現されれば let (変数)の「絶滅」に一歩近づいたと言えるでしょう。

ところで、元記事には const の利点について

constなら宣言された行だけを見ればどんな値が入っているかがわかりますが、letはコード全体を追う必要があり、読み手への負担が大きいです。

と書かれています。僕は、これは必ずしも真ではないと考えています。たとえば、変数と for ループを使って 1 から 100 までの合計を求めるコードは、次のように十分小さなスコープを切れば問題ありません。変数 _sum の値を追うのが辛いということはないでしょう6

// Swift
let sum: Int // 定数
do { // 変数を使う小さなスコープ
    var _sum = 0 // 変数
    for x in 1 ... 100 {
        _sum += x
    }
    sum = _sum
}

残念ながら、 JavaScript では const の初期化を遅らせられないのでこの手を使うことはできません。 ES2017 前提で似たようなことをするなら、次のように書くことになるでしょう。

// JavaScript (※引用でない)
const sum = (() => { // 変数を使う小さなスコープ
    let sum = 0; // 変数
    for (let i = 1; i <= 100; i++) {
        sum += i;
    }
    return sum;
})();

もしくは、 let (変数)を小さなスコープに留めることを諦めるかです。その場合でも、個々の関数やメソッドが十分に小さく保たれていれば、 let (変数)が問題になることは少ないのではないでしょうか。僕個人の体験を振り返ってみても、(ローカルスコープで)定数ではなく変数を使ったことによって問題が引き起きおこされたようなケースは、もう何年も記憶にありません。

少し違った視点

ここまで、 JavaScript と Swift で変数を「絶滅」させたコードを比較し、いくつかの言語仕様や標準ライブラリの API が提供されていれば、より可読性の高いコードが書けることを見てきました。しかし、変数をより「絶滅」させやすい Swift ですが、 Swift はむしろ変数をよく使う言語です。

もちろん、 Swift でも無駄に var を使うことは推奨されませんし、 let (定数)にできるのに var になっている箇所があるとコンパイラが警告してくれます。しかし、それは var をあまり使わない方が良いということではありません。

JavaScript をはじめ、 Java, Scala, Kotlin, C#, Python, Ruby などはすべて参照型中心の言語です。 Swift がそれらの言語と決定的に異なるのは、 Swift は値型中心の言語だということです。 Array などのコレクションを含め、 Swift の標準ライブラリの型のほとんどは値型です。

値型を扱う場合、 varlet (定数)かはミュータブルかイミュータブルかということに直結します。参照型中心の言語では Value Semantics を得るためにイミュータブルクラスが広く用いられますが、値型は基本的に Value Semantics を持っているため、イミュータビリティの重要性が高くありません。

それはさらに、

  • 参照型中心 → イミュータブルな型を多用 → 式による変更が多い → 式指向が便利 → 定数で済ませやすい
  • 値型中心 → ミュータブルな型を多用 → 文による変更が多い → 文指向が便利 → 変数を活用する場面が多い

とつながります。

この関係で言えば、 JavaScript は参照型中心の言語なので式指向や定数と相性が良いはずです。しかし、言語の構文が式指向でないので上記の関係がねじれてしまって、 const を活用しづらいという見方もできるのではないでしょうか。


  1. とはいえ、他ライブラリとの名前の衝突は面倒なので、標準ライブラリなどの型の API を拡張する場合は名前衝突に配慮が必要です。ただ、ライブラリのコードではなく、アプリのコードを書いている場合はほぼ気にする必要はありません。 Swift でアプリを書くときに、それぞれの環境で都合の良い独自の extension を書くことは当たり前に行われていて、危険もほぼありません。また、仮に衝突してしまっても、別ファイルで一つずつ import して別名を付けることで、最悪ケースでも衝突を回避することが可能です。 

  2. "On the road to Swift 6" の中で concurrency が挙げられています。 

  3. "Swift Concurrency Manifesto" の Part 1 として async/await が挙げられています。 async/await についての Proposal はこちらです。 

  4. 先日 async に関する PR が Swift のリポジトリにマージされました。 

  5. ただし、 Function Builder の if let への対応は Swift 5.3 ( 2020 年秋にリリース予定の次期マイナーバージョン)からの予定です。 

  6. ただし、このコードでは定数 sum の初期化がここで完結することが構文上保証されません。その観点で言えば、式指向の言語のように、スコープの最後の式をスコープの値として直接定数に代入できるとより良いと思います。 

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

【Swift】ゼロからのCombineフレームワーク - ユニットテストを書いてみる

Combineを使ったユニットテストの方法

2つの方法を試してみました。

  1. ライブラリなしでやる
  2. Entwineというテスト補助用のライブラリを使う

テスト対象コード

incrementCounter: PassthroughSubjectsendメソッドが呼ばれたら、自身のcounter: Intに数値を加えて、counterStr: CurrentValueSubjectを更新する単純なモデルです。

テストコードでは、incrementCountersendメソッドの呼び出しにたいして、counterStrが正しく更新されていることをテストします。

CounterViewModel.swift
import Combine
import Foundation

protocol CounterViewModelProtocol {
    var incrementCounter: PassthroughSubject<Int, Never> { get }
    var counterStr: CurrentValueSubject<String, Never>! { get }
}

class CounterViewModel: CounterViewModelProtocol {
    var incrementCounter: PassthroughSubject<Int, Never> = .init()
    var counterStr: CurrentValueSubject<String, Never>!

    private var counter: Int = 0
    private var cancellables = Set<AnyCancellable>()

    init() {
        counterStr = CurrentValueSubject("\(counter)")
        incrementCounter
            .sink(receiveValue: { [weak self] increment in
                if let self = self {
                    self.counter += increment
                    self.counterStr.send("\(self.counter)")
                }
            }).store(in: &cancellables)
    }
}

ライブラリなしでテストする

How to Test Your Combine Publishersを参考にしました。
テスト補助用のexpectValueというメソッドにPublisherと期待される値の配列を渡して、waitします。

CounterViewModelTests.swift
func testCounterStr() {
    let viewModel = CounterViewModel()        
    let expectValues = ["0", "2", "5"]
    let result = expectValue(of: viewModel.counterStr, equals: expectValues)
    viewModel.incrementCounter.send(2)
    viewModel.incrementCounter.send(3)
    wait(for: [result.expectation], timeout: 1)
}

テスト補助用のメソッド

extension XCTestCase {
    typealias CompetionResult = (expectation: XCTestExpectation, cancellable: AnyCancellable)
    func expectValue<T: Publisher>(
        of publisher: T,
        timeout: TimeInterval = 2,
        file: StaticString = #file,
        line: UInt = #line,
        equals: [T.Output]
    ) -> CompetionResult where T.Output: Equatable {
        let exp = expectation(description: "Correct values of " + String(describing: publisher))
        var mutableEquals = equals
        let cancellable = publisher
            .sink(receiveCompletion: { _ in },
                  receiveValue: { value in
                    if value == mutableEquals.first {
                        mutableEquals.remove(at: 0)
                        if mutableEquals.isEmpty {
                            exp.fulfill()
                        }
                    }
            })
        return (exp, cancellable)
    }
}

Entwineを使ってテストする

テスト用に用意されたTestSchedulerを使って、テスト対象のSubjectsendメソッド呼び出しのタイミングを設定したあと、resumeメソッドを呼び出します。

TestableSubscriberをテスト対象のPublisherreceiveすることで、TestableSubscriberrecordedOutputにイベントが記録されます。

func testCounterStrWithEntWine() {
    let scheduler = TestScheduler(initialClock: 0)
    let incrementCounter = viewModel.incrementCounter
    scheduler.schedule(after: 100) { incrementCounter.send(2) }
    scheduler.schedule(after: 200) { incrementCounter.send(3) }

    let subscriber = scheduler.createTestableSubscriber(String.self, Never.self)
    viewModel.counterStr.receive(subscriber: subscriber)

    scheduler.resume()

    let expected: TestSequence<String, Never> = [
        (000, .subscription),
        (000, .input("0")),
        (100, .input("2")),
        (200, .input("5")),
    ]

    XCTAssertEqual(subscriber.recordedOutput, expected)
}

参考

How to Test Your Combine Publishers
EntwineTest Reference

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

PickerViewの実装までの流れ

はじめに

pickerviewの実装について記事を書いていこうと思います。
駆け出しエンジニアということもあるのでご指摘などございましたらご教授よろしくお願いします。
(swift5.4.2)
まずPickerの実装をするまでの工程を紹介します。
1.UIPickerViewDelegate UIPickerViewDateSourceの定義と設定
2.UIPickerViewの列と行とデータ要素の数の設定
3.UIPickerViewの標示する配列の設定

ゴール地点!

AnyConv.com__画面収録 2020-08-04 22.20.16.gif

1.UIPickerViewDelegate UIPickerViewDateSourceの定義と設定について

まず
Delegateとは?
移譲や任せるなどという意味らしく、他にはデザインパターンなどという記述がありとても解釈が難しいところ。
なので私の抽象的な解釈を述べますと
「他のクラスに処理をお任せし、移譲する処理の流れのことをデザインパターンと言いたいのでは?」
とりあえずPickerの実装にDelegateが必要要素と覚えておきましょう!

Datesourceとは?
「テーブル(ここでいうpicker)の具体的な表示内容を決める」
以上です。ww

filename.rb
let datalist = ["item1","item2","item3","item4"]

これでpickerのドラムロールに入れたい値をかく!

filename.rb
import UIKit

class ViewController: UIViewController , UIPickerViewDelegate, UIPickerViewDataSource

これでclassにプロトコルであるUIPickerViewDelegate, UIPickerViewDataSource
の定義は完成しました

filename.rb
  pickerView.delegate = self delegateの設定
        pickerView.dataSource = self datesourceの設定

ここでは、delegateやdatesourceはどれを指しているのか?
self=自分自身
つまり、selfの自身とは、ViewControllerのことを指している。

以上

2.UIPickerViewの列と行とデータ要素の数の設定

filename.rb
//列数の設定
 func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
//行とデータ要素数の設定
func pickerView(_ pickerView: UIPickerView,
                    numberOfRowsInComponent component: Int) -> Int {
        return dataList.count
    }

このメソッドはpicker実装には必須要素になる。
列数の設定
メソッドの処理部分(return n)のnの部分を希望列数にする
行とデータ要素の数の設定
複数行にしたい場合、

filename.rb
case 0:
            return datelist1.count            
case 1:     return datelist2.count
default:
            return 0 

caseN N=行数
countは要素数を表すので要素数の変更したい場合は、datalistの処理を変更してあげるとcountが合わせてくれる!

3.UIPickerViewの標示する配列の設定

filename.rb
func pickerView(_ pickerView: UIPickerView,
                    titleForRow row: Int,
                    forComponent component: Int) -> String? {

        return dataList[row]
    }

このメソッドが無いとpickerのデータが反映されずに?の値が帰ってきます。
なので必須で書いてください

Rowとは?
おそらく、ViewControllerの行番号を指しますので
datalistの行番号を表示して!
という解釈でよろしいかと思います

終わりに

これでpickerの実装はされたと思います!
ゴール地点
では、pickerのデータがlabelに反映されていますが、そちらの方法は
また今度記事で書いていこうかと思います!

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

UISliderの停止位置をInt値に固定する

UISliderとは

UIKitで提供されている下記のようなUIパーツです。主につまみ部分(Thumb)をTrack上でドラッグすることで値を変えることができます。公式リファレンスの絵では、画面輝度の設定を想定したものになっています。

UISliderの構成要素(公式リファレンスより)

使い勝手が微妙に悪い…

標準で提供されているのですが、いくつか使い勝手が悪い点があります。

  • 値がFloat型になっており、Int型を扱いずらい
  • 目盛りの設置が難しい

今回、この辺りを試行錯誤してみました。

完成版

output.gif
サンプルプロジェクト
https://github.com/shcahill/IntSlider

ポイント

ポイントは以下です。

  • 最小値(minimumValue)は0固定
  • 最大値(maximumValue)はInt値
  • Sliderを中途半端な位置で止めた場合は四捨五入してInt値に丸めてThumbの位置を自動調整する
  • 1ごとに目盛りを配置
  • 目盛りはAutoLayoutを使用しているため、縦横切り替えにも追従可能(TinyConstraints使用)

あえて作り込まなかった点

また、今回のサンプルでは作り込まなかった点は以下のとおりです。

  • 目盛りのデザインはカスタマイズできない
  • コード上でInt以外の値を設定できてしまう

作り込まなかったのは私がサボっているだけですので、ご容赦ください…

実装方法

Thumbの停止位置をInt値に固定する

やり方としては、ドラッグの終了を検知したタイミングで、Thumb(つまみ)の位置を強制的にInt値の位置へ移動させます。

ドラッグの終了イベントの検知

ドラッグイベントはtouchesEndedをoverrideすることで検知できます。

IntSlider
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)
    // ドラッグ終了後にThumb位置を調整する
    fixSliderPosition()
}

Thumbの位置を調整する

Sliderの値はFloat型ですので、Int型へ丸め込みます。(Thumbの位置はSliderのvalueに追従します)
Int型への丸め込みは round関数を使って四捨五入でやっていますが、利用シーンによっては切り捨て・切り上げに変更しても良いかと思います。ただ、Thumをドラッグしたときの操作感的には四捨五入が良いのではというのが私見です。

IntSlider
/// Slideの値変更通知(四捨五入して整数で通知されます)
var onValueChanged: ((Int) -> Void)?

private func fixSliderPosition() {
    // 現在の値を四捨五入でIntに丸める
    let index = round(self.value)
    self.value = index

    // コールバック通知
    onValueChanged?(Int(index))
}

onValueChangedはコールバック通知ですので、読み飛ばしていただいても問題ありません。
ポイントとしては、値の変更通知はInt型として通知を行っているところです。

目盛りの表示

Thumbの停止位置(Int値の部分)に目盛りとなるViewを配置します。
Sliderが何段階設定が可能なのか、どこで止まるのか、ということを示すには重要なパーツになります。

目盛りの生成

まず、目盛りの生成箇所です。
maximumValueの値が変更される度に目盛りを作り直す必要があるため、目盛りのViewはフィールドで labelListとして保持します。
また、目盛りはStackViewに詰め込み、等間隔で並べています。

IntSlider
// Max値変更の度に目盛りを作り直す必要があるため、フィールドで保持
private var labelList = [UIView]()

/// 目盛りを貼りなおします
func updateScaleLabel() {
    // 生成済みのラベルをすべて剥がす
    labelList.forEach({ $0.removeFromSuperview() })
    labelList.removeAll()

    // StackViewで目盛りを等間隔で配置する
    let labelArea = UIStackView()
    labelArea.axis = .horizontal
    labelArea.distribution = .equalSpacing
    labelArea.alignment = .fill
    insertSubview(labelArea, at: 0)
    let max = Int(maximumValue) + 1
    // 目盛りの数だけStackViewに詰め込む
    for _ in 0..<max {
        let label = createLabel()
        labelArea.addArrangedSubview(label)
        // 目盛りを作り直せるように、配列に保持する
        labelList.append(label)
    }

    /** 目盛りエリアの位置調整(後述) */
    // trackの少し下方に配置(offsetの16は適当)
    labelArea.centerYToSuperview(offset: 16)
    // track左右のマージン
    let offset = thumbCenterOffset
    labelArea.leadingToSuperview(offset: offset)
    labelArea.trailingToSuperview(offset: offset)
}

ここで少し面倒なのが、目盛り表示エリア(labelArea)の左右のマージンthumbCenterOffsetの計算方法です。

目盛り表示エリアの配置設定

目盛りのX方向の始点は、trackのboundsのstartXではありません。
目盛りのX方向中心位置は、ThumbのcenterXと一致している必要があります。よって以下のようにthumbCenterOffsetを計算します。

IntSlider
/// trackの左右両端に対する、thumb中心X座標のマージン
var thumbCenterOffset: CGFloat {
    // trackの始点
    let startOffset = trackBounds.origin.x
    // valueが0のときのThumb位置を計算
    let firstThumbPosition = positionX(at: 0)
    // track/Thumb/目盛りのサイズからoffsetを計算
    return firstThumbPosition - startOffset - labelSize / 2
}

/// [index]のときのthumbのX中心座標を取得します
func positionX(at index: Int) -> CGFloat {
    let rect = thumbRect(forBounds: bounds, trackRect: trackBounds, value: Float(index))
    return rect.midX
}

var trackBounds: CGRect {
    return trackRect(forBounds: bounds)
}

絵にすると以下の感じです。

Slider.png

これでThumbの位置と目盛りの位置がすべて一致するようになります。

参考:UISliderのThumbの表示領域(SizeやFrame)を計算するExtension

目盛りViewの生成

ここでは簡単にするために単純なドットにしていて、カスタマイズもできないようになっています。

private let labelSize: CGFloat = 4.0

func createLabel() -> UIView {
    let label = UIView()
    label.backgroundColor = .black
    label.layer.cornerRadius = CGFloat(labelSize / 2)
    label.width(labelSize)
    label.height(labelSize)
    return label
}

SliderのmaximumValueの値変更

SliderのmaximumValueの値が変更された場合は、目盛りの数と配置が変わるため、上記のupdateScaleLabel()を呼ぶ必要があります。

IntSlider
func updateMaxValue(_ max: Int) {
    maximumValue = Float(max)
    value = min(value, maximumValue)
    updateScaleLabel()
}

ここでは関数化しましたが、maximumValueのdidSetで実行するのもいいかもしれません。(その場合、Float型をInt型に補正する必要がありますが。)

Thumb部分以外でもドラッグ可能にする

詳細はこちらの記事が参考になります。
UISliderのUXをトコトン追究して改善してみる
必要なコードだけを抜き出すと以下のようになります。

IntSlider
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    // つまみ部分以外でもスライド可能
    return true
}

完成

以上をまとめると、コード全体は以下のようになります。

IntSlider
import UIKit
import TinyConstraints

final class IntSlider: UISlider {
    private let labelSize: CGFloat = 4.0
    /// Slideの値変更通知(四捨五入して整数で通知されます)
    var onValueChanged: ((Int) -> Void)?
    private var labelList = [UIView]()

    required init?(coder aDecoder: NSCoder) {
        super.init(coder:aDecoder)
        setup(max: 1)
    }

    private func setup(max: Int) {
        minimumValue = 0
        maximumValue = Float(max)

        // リアルタイムの値変更通知
        addTarget(self, action: #selector(onChange), for: .valueChanged)
    }

    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        // つまみ部分以外でもスライド可能
        return true
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        // スライド終了後に位置を調整する
        fixSliderPosition()
    }

    @objc func onChange(_ sender: UISlider) {
        // スライダーの値が変更された時の処理
        onValueChanged?(Int(round(sender.value)))
    }

    func updateMaxValue(_ max: Int) {
        maximumValue = Float(max)
        value = min(value, maximumValue)
        updateScaleLabel()
    }
}

private extension IntSlider {
    func fixSliderPosition() {
        let index = round(self.value)
        self.value = index
        onValueChanged?(Int(index))
    }

    /// 目盛りを貼りなおします
    func updateScaleLabel() {
        labelList.forEach({ $0.removeFromSuperview() })
        labelList.removeAll()
        let labelArea = UIStackView()
        labelArea.axis = .horizontal
        labelArea.distribution = .equalSpacing
        labelArea.alignment = .fill
        insertSubview(labelArea, at: 0)
        // trackの少し下方
        labelArea.centerYToSuperview(offset: 16)
        // 左右のマージン
        let offset = thumbCenterOffset
        labelArea.leadingToSuperview(offset: offset)
        labelArea.trailingToSuperview(offset: offset)
        let max = Int(maximumValue) + 1
        for _ in 0..<max {
            let label = createLabel()
            labelArea.addArrangedSubview(label)
            labelList.append(label)
        }
    }

    /// 目盛りViewの生成
    func createLabel() -> UIView {
        let label = UIView()
        label.backgroundColor = .black
        label.layer.cornerRadius = CGFloat(labelSize / 2)
        label.width(labelSize)
        label.height(labelSize)
        return label
    }

    /// trackの左右両端に対する、thumb中心X座標のマージン
    var thumbCenterOffset: CGFloat {
        let startOffset = trackBounds.origin.x
        let firstThumbPosition = positionX(at: 0)
        return firstThumbPosition - startOffset - labelSize / 2
    }

    /// [index]のときのthumbのX中心座標を取得します
    func positionX(at index: Int) -> CGFloat {
        let rect = thumbRect(forBounds: bounds, trackRect: trackBounds, value: Float(index))
        return rect.midX
    }

    var trackBounds: CGRect {
        return trackRect(forBounds: bounds)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Xcode] Xcodeのディレクトリ構成と実際のプロジェクト構成の乖離を修正する

概要

あんまりないケースですが実務上、Xcodeで指定のディレクトリ上でファイルを新規追加したつもりがFinderで確認すると、全然違う箇所に作成されていた・・・というケースがあり困りました :sob:

(新規作成時に表示されるウィンドウ内のFinder上でどこに配置されるかをちゃんと見れば、問題は回避できますが初期表示で出ているディレクトリが全然違う場所を指している現象がよくわかっていません・・・)

前提

  • Xcode11.5
  • Xcodeのディレクトリ構成が正
  • Finder上の実際のディレクトリ構成が誤

とします

Xcodeのディレクトリ構成自体が間違いの場合は以下の記事を書いた人のが参考になります↓
【Xcode】開発途中に手動でディレクトリ構成を変更する

Xcodeのディレクトリ構成は↓
スクリーンショット 2020-08-05 0.56.46.png

実際のディレクトリ構成は↓
スクリーンショット 2020-08-05 0.57.16.png
という例とします

本来、Xcodeのディレクトリ構成のようにSampleAにSampleA.swiftファイルが配置されてるのが想定であるのに、実際のディレクトリ構成はSampleBにSampleA.swiftファイルが配置されているのは想定していないだったとします

修正方法

  1. 実際に配置したい箇所にファイルを移動する(コマンド打ってもFinderでドラッグアンドドロップしてもやり方お任せ)
  2. Xcodeのボタンで再配置 赤くなったファイルを消して、Finderからドラッグアンドドロップでもいいのですが 下スクショ内にあるボタンタップでも大丈夫です 修正したいファイルをタップ > Identity and Typeタップ > Location?のボタンタップ

スクリーンショット 2020-08-05 1.19.10.png


それで1で移動したファイルを選択してあげれば修正できます
スクリーンショット 2020-08-05 1.22.14.png

参考になれば幸いです :pray:

備考

Locationの項目にあるボタン名の名称知っている方教えてください :upside_down:

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