- 投稿日:2020-08-05T23:11:44+09:00
【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
の時も色の変更が可能。
プロジェクトでレイアウトを統一する時は共通コンポーネントとして用意しておくと便利ですね。
- 投稿日:2020-08-05T23:03:50+09:00
日本人のための SwiftFormat【ルール編】
ルール一覧
SwiftFormat の Rules 一覧を日本語でまとめました。
andOperator
if
,guard
,while
の条件分岐の際の条件式で&&
を使う場合はカンマを使うようにする。
サンプル
- if true && true { + if true, true {anyObjectProtocol
プロトコルの定義で、
class
をAnyObject
に変更する。※
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: Intbraces
字下げスタイルを選択する。(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 = 5duplicateImports
重複する import 文を削除する。
サンプル
import Foo import Bar - import Fooimport B #if os(iOS) import A - import B #endifelseOnSameLine
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 == 0
をisEmpty
に変換する※ まれに、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.123preferKeyPath
簡易的な
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 _ = barredundantRawValues
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-only
はinit
内でのみ有効。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 = 6spaceAroundBraces
中括弧周りのスペースを追加または削除する。
サンプル
- 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) -> BoolspaceAroundParens
括弧の前後のスペースを追加または削除する。
サンプル
- 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: - UIScrollViewDelegatetrailingClosures
該当する場合は、末尾の閉鎖構文を使用する。
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() -> Voidwrap
指定された最大幅を超える行を折り返す。
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
式の右側にある定数値を優先する。
- 投稿日:2020-08-05T22:26:07+09:00
複数のボタンを、1つの変数でまとめる。
※オブジェクト
(Label とか Buttonとか Text Field とかもオブジェクト。)
(Swiftは、オブジェクト指向言語。)
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典複数のオブジェクトを、1つの変数でまとめる。
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年前、藤原道長も同じ月を見ていたを思うと、しみじみシジミ。
- 投稿日:2020-08-05T22:25:44+09:00
ボタンの角を丸くする?
- 投稿日:2020-08-05T20:57:04+09:00
[swift5]エラーNo such module 'ライブラリ名'
エラー状況
画像のようにXcodeでビルドを行うと
No such module 'ライブラリ名'
というエラーが発生する。
対応したこと
①Xcodeの再起動
②Macの再起動
③podfileの確認とpod install
を再度実行①〜③全て不発...
検索を行い色々と模索していると解決しそうな記事に遭遇!
https://qiita.com/doge_kun55/items/26ac2fa2031ddbeee879解決方法
Xcodeのツールバーにある
Product/Scheme/ManageSchemes/
を選択。
すると以下のようにページが開く。
今回の場合はデフォルトで
Swift5Bokete
にチェックがついており、その他にはチェックはついていませんでした。
なので全ての項目のshow
とshared
にチェックをつけてClose
をクリック。そのあと、再ビルドを実行すると無事ビルドされました。
最後に
ちなみに、今回エラーの解決はしていますが、何が原因で、どういう理由で解決に至ったかということが理解できておりません。もし、この記事を見た方の中に原因をわかる方がおられましたらご教授の程宜しくお願い致します。
- 投稿日:2020-08-05T19:44:03+09:00
Buildしたけど真っ黒、アルアル原因まとめ
Buildしたけど真っ黒、アルアル原因まとめ
2点。
1, NSUnknownKeyException
お笑い養成所(NSC)ではなく、NS Unknown〜(※)です。
ViewController
とMain StoryBoard
を繋げるとき、 やっぱ繋げるのやめようってなって コードを削除しても、繋がりは切れません。不要な接続が繋がったままだから、エラーが起きるみたい。
NSUnknownKeyExceptionが出た時の回避方法ユーティリティーエリアの、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長押し、表示を維持)
(一回ドラッグ&ドロップすれば、あとは長押し続けなくても表示を維持)終わりに
久々に見たけど、
BackNumber「高嶺の花子さん」MV 神。
- 投稿日:2020-08-05T13:52:38+09:00
サードパーティ製アプリを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/ では
- ペアレンタルコントロール機能もしくはロックダウンモードを持つこと
- セーフブラウジング、もしくはフィッシングなどの問題への警告を表示すること
も要件としていましたが、これについてはこちらのツイート
iOSデフォルトブラウザの要件がやっと公式発表された!
— 加藤雄一@Smooz / 在庫速報.com (@sassymanyuichi) August 5, 2020
ただ、このページのhttps://t.co/yFb0EaLveh
下記の2つの要件はMust項目では無いのでは
・ペアレンタルコントロール機能もしくはロックダウンモードを持つこと
・セーフブラウジング、もしくはフィッシングなどの問題への警告を表示すること
で指摘されている通り
「ペアレンタルやロックダウンモード機能」と「セーフブラウジング、フィッシングへの警告表示」は必須項目ではないと思います。標準ブラウザ資格を持ったアプリに対する制限
標準ブラウザとなった場合にはユーザのWeb閲覧を独占するため、個人データへの不要なアクセスは回避すべきとの方針。
Info.plistで以下のキーを持っているとリジェクト
NSPhotoLibraryUsageDescription
→ 写真を取得する際はPHPickerViewControllerを使用する。「NSPhotoLibraryAddUsageDescription」の使用はOK。NSLocationAlwaysUsageDescription、NSLocationAlwaysAndWhenInUseUsageDescription
→ 代わりに「NSLocationWhenInUseUsageDescription」を使用するNSHomeKitUsageDescription
→ HomeKitへのアクセス禁止NSBluetoothAlwaysUsageDescription
→ 代わりに「NSBluetoothWhileInUseUsageDescription」を使用NSHealthShareUsageDescription、NSHealthUpdateUsageDescription
→ ヘルスデータへのアクセス禁止参考
- 投稿日:2020-08-05T12:19:38+09:00
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.swiftMarketingCloudSDK.sharedInstance().sfmc_setURLHandlingDelegate(self)上記コードをSDKの初期設定記述の後に、AppDelegate.swift内に記述する。
その後、
MarketingCloudSDKURLHandlingDelegate
プロトコルメソッドを追加AppDelegate.swiftextension 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...を選択する
Notification Service Extensionを選択してNextをクリックする
productnameを入力してfinishボタンをクリックする
上記のようなポップアップがでてきたら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
- 投稿日:2020-08-05T12:04:17+09:00
tableviewのサイズを中身のサイズに合わせる。
- 投稿日:2020-08-05T10:44:36+09:00
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
(定数)が省略されていますが、i
はlet
(定数)で宣言されます。0 ..< 10
は標準ライブラリのRange
を生成します。元記事の JavaScript のコードと同じように
forEach
を使って書くこともできます。しかし、forEach
はbreak
やcontinue
,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
の他にSet
やRange
など任意のSequence
に対して利用できます。// Swift let sum = (1 ... 10).sum() // 55なお、 Swift では標準ライブラリの型に独自のメソッドを追加しても、 JavaScript と比べると衝突の心配は小さいです。 Swift では同名のメソッドを追加した複数のライブラリを同時に利用しても、それぞれ異なるシンボル名にコンパイルされているので干渉し合うことはありません。同一ファイル上でそれらを複数
import
した場合はコード上でどれを指しているか曖昧になるのでコンパイルエラーになりますが、それは別名を付けるなどして回避可能です。1JavaScript でも
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
してage
のArray
に変換してから合計した方が読みやすいでしょう。しかし、その場合、map
は中間計算のためだけにArray
インスタンスを生成することになり無駄です(何百万個の要素を持つ巨大なArray
かもしれません)。LazySequence
を使えばそのような問題を解決できます。// Swift let sumOfAge = users.lazy.map(\.age).reduce(0, +)
LazySequence
のmap
による変換は即時実行されません。上記コードではreduce
時に遅延して要素を取り出し、要素ごとにmap
の変換処理が行われます。そのため、中間計算のために巨大な実体を持ったコレクションが生成されることを防げます。 JavaScript でも、これに相当する手段を標準で提供してくれれば、パフォーマンスを犠牲にせず可読性を向上させることができるでしょう。またこの例のように、
User
のArray
をage
のArray
に変換したいようなケースは多いと思います。 Swift のKeyPath
(\.age
)はそのようなケースで役立ちます。 Lodash を使った一つ目の例では'age'
を文字列として渡して似たようなことをしていますが、\.age
は文字列ではなく静的型検査可能なのでより安全です(そもそも JavaScript は動的型付けですが)。if文
// JavaScript const tax = isTakeout ? 0.08 : 0.1Swift でも同様に三項演算子が使えます。
// 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/catch
とasync/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
(定数)を組み合わせて、初期化を遅延させられることが利いています。if
やswitch
による分岐だけでなく、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
は値型なのでvar
かlet
かを変更するだけでミュータビリティをコントロールすることができます。また、 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
です。String
をInt
に変換する処理は、Int("42")
ような場合は成功しますがInt("ABC")
のような場合には失敗します。Int.init
は失敗したときにnil
を返しますが、compactMapValues
はmapValues
した上で結果がnil
になるエントリーを取り除いてくれます。JavaScript から
let
を「絶滅」させるために足りないものこうして色々なケースを比較して眺めてみると、 JavaScript から
let
(変数)を「絶滅」させる一番のハードルは、const
と制御構文の相性の悪さではないでしょうか。特に、try/catch
やasync/await
をif
やswitch
と組み合わせて使おうとすると途端に辛くなってしまいます。もし JavaScript に改変を加えられるとして、この問題を解決するために僕がすぐに思いつく選択肢は次の二つです。
- Swift のように、制御フローを解析して網羅的に初期化されている場合は
const
の初期化を遅延させられるようにする。- Scala や Kotlin のように、
if
やswitch
等を式にする。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/catch
やasync/await
などと組み合わせてもこれができることが求められます。他にも、上記で比較してみた範囲でも、
- 演算子が関数でない
map
やreduce
を遅延させられない(ので中間計算のために実体を伴う無駄なコレクションを生成しないといけない)- 標準で足りない道具が多いわりに
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 の標準ライブラリの型のほとんどは値型です。値型を扱う場合、
var
かlet
(定数)かはミュータブルかイミュータブルかということに直結します。参照型中心の言語では Value Semantics を得るためにイミュータブルクラスが広く用いられますが、値型は基本的に Value Semantics を持っているため、イミュータビリティの重要性が高くありません。それはさらに、
- 参照型中心 → イミュータブルな型を多用 → 式による変更が多い → 式指向が便利 → 定数で済ませやすい
- 値型中心 → ミュータブルな型を多用 → 文による変更が多い → 文指向が便利 → 変数を活用する場面が多い
とつながります。
この関係で言えば、 JavaScript は参照型中心の言語なので式指向や定数と相性が良いはずです。しかし、言語の構文が式指向でないので上記の関係がねじれてしまって、
const
を活用しづらいという見方もできるのではないでしょうか。
とはいえ、他ライブラリとの名前の衝突は面倒なので、標準ライブラリなどの型の API を拡張する場合は名前衝突に配慮が必要です。ただ、ライブラリのコードではなく、アプリのコードを書いている場合はほぼ気にする必要はありません。 Swift でアプリを書くときに、それぞれの環境で都合の良い独自の
extension
を書くことは当たり前に行われていて、危険もほぼありません。また、仮に衝突してしまっても、別ファイルで一つずつimport
して別名を付けることで、最悪ケースでも衝突を回避することが可能です。 ↩"On the road to Swift 6" の中で concurrency が挙げられています。 ↩
"Swift Concurrency Manifesto" の Part 1 として
async/await
が挙げられています。async/await
についての Proposal はこちらです。 ↩ただし、 Function Builder の
if let
への対応は Swift 5.3 ( 2020 年秋にリリース予定の次期マイナーバージョン)からの予定です。 ↩ただし、このコードでは定数
sum
の初期化がここで完結することが構文上保証されません。その観点で言えば、式指向の言語のように、スコープの最後の式をスコープの値として直接定数に代入できるとより良いと思います。 ↩
- 投稿日:2020-08-05T08:40:36+09:00
【Swift】ゼロからのCombineフレームワーク - ユニットテストを書いてみる
Combineを使ったユニットテストの方法
2つの方法を試してみました。
- ライブラリなしでやる
- Entwineというテスト補助用のライブラリを使う
テスト対象コード
incrementCounter: PassthroughSubject
のsend
メソッドが呼ばれたら、自身のcounter: Int
に数値を加えて、counterStr: CurrentValueSubject
を更新する単純なモデルです。テストコードでは、
incrementCounter
のsend
メソッドの呼び出しにたいして、counterStr
が正しく更新されていることをテストします。CounterViewModel.swiftimport 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.swiftfunc 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
を使って、テスト対象のSubject
のsend
メソッド呼び出しのタイミングを設定したあと、resume
メソッドを呼び出します。
TestableSubscriber
をテスト対象のPublisher
にreceive
することで、TestableSubscriber
のrecordedOutput
にイベントが記録されます。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) }参考
- 投稿日:2020-08-05T03:28:42+09:00
PickerViewの実装までの流れ
はじめに
pickerviewの実装について記事を書いていこうと思います。
駆け出しエンジニアということもあるのでご指摘などございましたらご教授よろしくお願いします。
(swift5.4.2)
まずPickerの実装をするまでの工程を紹介します。
1.UIPickerViewDelegate UIPickerViewDateSourceの定義と設定
2.UIPickerViewの列と行とデータ要素の数の設定
3.UIPickerViewの標示する配列の設定ゴール地点!
1.UIPickerViewDelegate UIPickerViewDateSourceの定義と設定について
まず
Delegateとは?
移譲や任せるなどという意味らしく、他にはデザインパターンなどという記述がありとても解釈が難しいところ。
なので私の抽象的な解釈を述べますと
「他のクラスに処理をお任せし、移譲する処理の流れのことをデザインパターンと言いたいのでは?」
とりあえずPickerの実装にDelegateが必要要素と覚えておきましょう!Datesourceとは?
「テーブル(ここでいうpicker)の具体的な表示内容を決める」
以上です。wwfilename.rblet datalist = ["item1","item2","item3","item4"]これでpickerのドラムロールに入れたい値をかく!
filename.rbimport UIKit class ViewController: UIViewController , UIPickerViewDelegate, UIPickerViewDataSourceこれでclassにプロトコルであるUIPickerViewDelegate, UIPickerViewDataSource
の定義は完成しましたfilename.rbpickerView.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.rbcase 0: return datelist1.count case 1: return datelist2.count default: return 0caseN N=行数
countは要素数を表すので要素数の変更したい場合は、datalistの処理を変更してあげるとcountが合わせてくれる!3.UIPickerViewの標示する配列の設定
filename.rbfunc pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return dataList[row] }このメソッドが無いとpickerのデータが反映されずに?の値が帰ってきます。
なので必須で書いてくださいRowとは?
おそらく、ViewControllerの行番号を指しますので
datalistの行番号を表示して!
という解釈でよろしいかと思います終わりに
これでpickerの実装はされたと思います!
ゴール地点
では、pickerのデータがlabelに反映されていますが、そちらの方法は
また今度記事で書いていこうかと思います!
- 投稿日:2020-08-05T01:32:42+09:00
UISliderの停止位置をInt値に固定する
UISliderとは
UIKitで提供されている下記のようなUIパーツです。主につまみ部分(Thumb)をTrack上でドラッグすることで値を変えることができます。公式リファレンスの絵では、画面輝度の設定を想定したものになっています。
使い勝手が微妙に悪い…
標準で提供されているのですが、いくつか使い勝手が悪い点があります。
- 値がFloat型になっており、Int型を扱いずらい
- 目盛りの設置が難しい
今回、この辺りを試行錯誤してみました。
完成版
サンプルプロジェクト
https://github.com/shcahill/IntSliderポイント
ポイントは以下です。
- 最小値(
minimumValue
)は0固定- 最大値(
maximumValue
)はInt値- Sliderを中途半端な位置で止めた場合は四捨五入してInt値に丸めてThumbの位置を自動調整する
- 1ごとに目盛りを配置
- 目盛りはAutoLayoutを使用しているため、縦横切り替えにも追従可能(TinyConstraints使用)
あえて作り込まなかった点
また、今回のサンプルでは作り込まなかった点は以下のとおりです。
- 目盛りのデザインはカスタマイズできない
- コード上でInt以外の値を設定できてしまう
作り込まなかったのは私がサボっているだけですので、ご容赦ください…
実装方法
Thumbの停止位置をInt値に固定する
やり方としては、ドラッグの終了を検知したタイミングで、Thumb(つまみ)の位置を強制的にInt値の位置へ移動させます。
ドラッグの終了イベントの検知
ドラッグイベントは
touchesEnded
をoverrideすることで検知できます。IntSlideroverride 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) }絵にすると以下の感じです。
これで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()
を呼ぶ必要があります。IntSliderfunc updateMaxValue(_ max: Int) { maximumValue = Float(max) value = min(value, maximumValue) updateScaleLabel() }ここでは関数化しましたが、
maximumValue
のdidSetで実行するのもいいかもしれません。(その場合、Float型をInt型に補正する必要がありますが。)Thumb部分以外でもドラッグ可能にする
詳細はこちらの記事が参考になります。
UISliderのUXをトコトン追究して改善してみる
必要なコードだけを抜き出すと以下のようになります。IntSlideroverride func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { // つまみ部分以外でもスライド可能 return true }完成
以上をまとめると、コード全体は以下のようになります。
IntSliderimport 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) } }
- 投稿日:2020-08-05T01:27:15+09:00
[Xcode] Xcodeのディレクトリ構成と実際のプロジェクト構成の乖離を修正する
概要
あんまりないケースですが実務上、Xcodeで指定のディレクトリ上でファイルを新規追加したつもりがFinderで確認すると、全然違う箇所に作成されていた・・・というケースがあり困りました
(新規作成時に表示されるウィンドウ内のFinder上でどこに配置されるかをちゃんと見れば、問題は回避できますが初期表示で出ているディレクトリが全然違う場所を指している現象がよくわかっていません・・・)
前提
- Xcode11.5
- Xcodeのディレクトリ構成が正
- Finder上の実際のディレクトリ構成が誤
とします
Xcodeのディレクトリ構成自体が間違いの場合は以下の記事を書いた人のが参考になります↓
【Xcode】開発途中に手動でディレクトリ構成を変更する本来、Xcodeのディレクトリ構成のようにSampleAに
SampleA.swift
ファイルが配置されてるのが想定であるのに、実際のディレクトリ構成はSampleBにSampleA.swift
ファイルが配置されているのは想定していないだったとします修正方法
- 実際に配置したい箇所にファイルを移動する(コマンド打ってもFinderでドラッグアンドドロップしてもやり方お任せ)
- Xcodeのボタンで再配置 赤くなったファイルを消して、Finderからドラッグアンドドロップでもいいのですが 下スクショ内にあるボタンタップでも大丈夫です 修正したいファイルをタップ > Identity and Typeタップ > Location?のボタンタップ
↓
それで1で移動したファイルを選択してあげれば修正できます
参考になれば幸いです
備考
Locationの項目にあるボタン名の名称知っている方教えてください