- 投稿日:2020-08-20T23:19:11+09:00
プライマリ言語を英語にして配信する際の勘所【AppStore】
この記事は
リリース作業などはしばらくやっていないと忘れますね。
AppStoreConnectでプライマリ言語を英語にするか日本語にするか悩んでる方向けに書きました。プライマリ言語
AppStoreは国別にあり、デフォルトでは全ての国に対してアプリを配信することができます。
プライマリ言語設定を英語(アメリカ)にしてリリースすると、ローカライズされていない国に関してはプライマリ言語設定が優先されます。ローカライズされていない国に関してはプライマリー言語(英語)が優先されるということは、北米や、英語のアジア圏の各国、イギリス、インドなどの英語を利用している国々対してアプリをリーチすることができます。
ではプライマリ言語設定を
日本語設定にしてリリースするとどうなるかというと。
日本以外の諸外国でかつローカライズをしていない場合、日本語のApp名、概要、スクリーンショット、プロモーションテキスト、キーワードなどが適応されるので、日本語で検索しないとアプリが引っかからないため、世界に対してアプリをリーチすることができなくなります。結論
主要言語設定を
英語(アメリカ)にして日本語はローカライズとしてリリースする。
日本を除き他の所外国に関しては英語が優先されるので、App名、キーワード、概要などが英語で表示されます。
仮にアプリをいつか中国エリアにも対応したい場合は、中国語(簡体or繁体)のローカライズを追加というように国別に言語を追加していくことが理想です。プライマリ言語を英語にした際に必要なもの
アプリ
- アプリ内で使用する文言
- アプリ内で使用する素材
AppStore
- アプリ名
- 概要
- キーワード
- サブタイトル
- プロモーション用テキスト
- スクリーンショット
必ずしもローカライズする必要ない?
- プライバシーポリシー
- サポートURL
プライバシーポリシーページは日本語・英語で記載があるといい思います。
サポートページは英語対応していたら海外のユーザーからフィードバック等があると思います。時間的に制約がある場合は、AppStore用だけでも用意すればいいのかなと思います。
参考
- 投稿日:2020-08-20T22:54:09+09:00
UITableviewをスクロールした際、Section headerを固定しない
自分用メモ
SwiftでUItableviewを使っている時、スクロールしたときにHeaderが画面に残るのをFloatにする。
コード
func scrollViewDidScroll(_ scrollView: UIScrollView) { let sectionHeaderHeight:CGFloat = 40; if (scrollView.contentOffset.y <= sectionHeaderHeight && scrollView.contentOffset.y >= 0) { scrollView.contentInset = UIEdgeInsets(top: -scrollView.contentOffset.y, left: 0, bottom: 0, right: 0); } else if (scrollView.contentOffset.y >= sectionHeaderHeight) { scrollView.contentInset = UIEdgeInsets(top: -sectionHeaderHeight, left: 0, bottom: 0, right: 0); } }
- 投稿日:2020-08-20T21:11:14+09:00
【SwiftUI】NavigationView タイトルカスタマイズ方法
タイトルとアクション等の設定
NavigationView に属性をつけるのではなく中身につけることに注意。
NavigationView { VStack { Text("How to customize view") } .navigationBarTitle(Text("Hello")) .navigationBarItems(trailing: Button("Done", action: { // Action // 左に作るときは leading: にする })) // .inline .automatic .large .navigationBarTitleDisplayMode(.automatic) .navigationBarBackButtonHidden(true) }タイトルのフォント/背景色の変え方
appearance()はあまり使わない方がいいという意見もありますが、これがとりあえず一番やりやすいかと思います。
UIViewController の属性を引き継いでいるので、中身はUIFont UIColor で設定します。struct MyView: View { init() { // 背景を透明にする UINavigationBarAppearance().configureWithTransparentBackground() // コンパクトなナビゲーションバーの時の背景色を変える UINavigationBar.appearance().barTintColor = .systemRed // コンパクトなナビゲーションバーのタイトル属性 UINavigationBar.appearance().titleTextAttributes = [ .font: UIFont.systemFont(ofSize: 10) .foregroundColor: .systemBlue!, ] UINavigationBar.appearance().largeTitleTextAttributes = [ .font: UIFont.systemFont(ofSize: 20) .foregroundColor: .systemBlue!, ] } var body: some View { // Navigation View } }
- 投稿日:2020-08-20T19:03:13+09:00
【今日からできる】SwiftUI/Combineのユニットテスト
こちらの記事は、YUMEMI.swift #9 ~テストと自動化~で発表した内容をもとに書いたものです。
サンプルプロジェクトはこちらにあります。
SwiftUIには公式のテスト用APIが用意されていない
UIKitなら例えば以下のようにテストを行うことができます。
let viewController = ViewController() // viewDidLoadを呼ぶためのメソッド viewController.loadViewIfNeeded() // subviewsからボタンを検索 let button = viewController .view .subviews .first(where: { $0 is UIButton }) // 存在確認 XCTAssertNotNil(button) // タップイベントを発火 button!.sendActions(for: .touchUpInside)SwiftUIのViewがユニットテストできないなんて誰が言ったの?
検索してみると……
Who said we cannot unit test SwiftUI views?1
という記事がヒットしました。どうやらこの記事を書いた人がViewInspectorというテスト用のフレームワークを作成してくれているそうです。
現状では、SwiftUIのユニットテストができる唯一無二のフレームワークで、公式のフレームワークが出るまではスタンダードになっていきそうな予感がします。
ViewInspector
こんな感じのAPIが用意されています。
// ボタンタップを実行 let button = try view .inspect() .hStack() .button(1) try button.tap() // onAppearをシミュレート let list = try view .inspect() .list() try list[5] .view(RowItemView.self) .callOnAppear()実際に使ってみる
サンプルのプロジェクトはよくある、GitHubのリポジトリを検索してくるやつです。
こんな感じのUIで、Listの下に"Search More Swift"というButtonが表示されています。これが表示されているかどうかをユニットテストで確認していきます。
準備:DIのためにprotocol経由で参照を持たせる
テストしやすいようにViewにprotocol経由でViewModelを持たせます。
// View struct SearchRepositoriesView<ViewModel: ViewModelProtocol>: View { // ObservableObject protocolがassociatedtypeを持っているため // protocolのまま型指定できないので、Generic Typeを利用する @ObservedObject private var viewModel: ViewModel ... } // ViewModel protocol ViewModelProtocol: ObservableObject { var lastQuery: String? { get } var state: ViewModelState { get } // Binding<Bool>として使うのでsetが必要 var shouldShowErrorAlert: Bool { get set } var repositoryList: [GitHubRepo] { get } var moreSearchResultsExist: Bool { get } func send(event: SearchRepositoriesViewModelEvent) }ViewInspectorを使って要素を取得する
注意点として、「子要素を複数持つ可能性のある要素」から子要素を取得するときには、子要素(例えばVStack)に対応したメソッド(
vStack)に、そのindex(親要素の中で何番目にあるか)を渡す必要があります。テストコード// 対象のViewにInspectable protocolを追加 extension SearchRepositoriesView: Inspectable {} ... // ViewModelのスタブを作成 stubViewModel = SearchRepositoriesViewModelStub() // 依存性注入 view = SearchRepositoriesView(viewModel: stubViewModel) stubViewModel.state = .ready stubViewModel.repositoryList = ... stubViewModel.moreResultsExist = true let text = try view!.inspect() .zStack() .vStack(0) // Zstackのindex:0の位置 .list(1) // Vstackのindex:1の位置 .button(1) // Listのindex:1の位置 .text() XCTAssertEqual(try text.string(), "Search More ")Viewの構造ZStack { VStack { // index: 0 SearchBar(...) // index: 0 List { // index: 1 ForEach(...) { ... } // index: 0 if ... { Text("No results found...") } else if self.viewModel.moreResultsExist { Button(action: { ... }) { // index: 1 if self.isLoading { ActivityIndicator() } else { (Text("Search More ") + // Target!!! Text(self.lastQuery) .font(.headline) .bold() .italic() ) } } } } } if isLoading && isListEmpty { ActivityIndicatorWithRectangle() } }カスタムViewの存在確認
今度はTextではなく、カスタムViewが表示されていることを確認します。
先ほどの"Search More Swift" Buttonをタップすると、インディケーターが周りだします。テストコードstubViewModel.state = .loading stubViewModel.repositoryList = ... stubViewModel.moreResultsExist = true let button = try view!.inspect() .zStack() .vStack(0) // Zstackのindex:0の位置 .list(1) // Vstackのindex:1の位置 .button(1) // Listのindex:1の位置 // カスタムViewを取り出す // viewメソッドにstructのselfを渡したあと、actualViewメソッドで // ActivityIndicatorのインスタンスが取得できる XCTAssertNoThrow( try button .view(ActivityIndicator.self) .actualView() ) // (おまけ)Textが入っていないことも確認 XCTAssertThrowsError(try button.text())ActivityIndicatorstruct ActivityIndicator: UIViewRepresentable { func makeUIView( context: Context ) -> UIActivityIndicatorView { let indicator = UIActivityIndicatorView() indicator.startAnimating() return indicator } ... }Buttonの内部(再掲)Button(action: { ... }) { if self.isLoading { ActivityIndicator() // Target! } else { (Text("Search More ") + Text(self.lastQuery) .font(.headline) .bold() .italic() ) } }SwiftUIのユニットテスト → やりやすい
SwiftUIではViewが状態の関数であることが強制されるおかげで、何をテストすべきかがわかりやすいため、ユニットテストが書きやすいです。
Combineのユニットテスト
CombineのユーティリティフレームワークであるEntwineを使います。
テスト補助用のクラスとしてTestSchedulerが用意されているので、これを使ってViewModelのテストをやってみます。
ViewModelprotocol ViewModelProtocol: ObservableObject { // 最後に使ったクエリを取得できる var lastQuery: String? { get } // View側からイベント通知をするためのメソッド func send(event: SearchRepositoriesViewModelEvent) ... } class ViewModel: ViewModelProtocol { // サーバーとの通信を行うModel private let reposModel: GitHubReposModelProtocol // 最後に使ったクエリは@Publishedをつけて、SwiftUI Viewの監視対象 @Published private(set) var lastQuery: String? ... }スケジューラーにイベントの通知をスケジューリングしていきます。
stubModel = GitHubReposModelStub() viewModel = SearchRepositoriesViewModel(model: stubModel) let scheduler = TestScheduler.init(initialClock: 0) // t = 100に "Swift" という queryを発行 scheduler.schedule(after: 100) { self.viewModel.send(event: .didSearchButtonClicked(query: "Swift")) } // t = 200に "Python" という queryを発行 scheduler.schedule(after: 200) { self.viewModel.send(event: .didSearchButtonClicked(query: "Python")) }テスト用のsubscriberを作成して、テスト対象であるviewModel.$lastQueryにsubscribeします。
// テスト用のsubscriberを作成 let subscriber = scheduler.createTestableSubscriber(String?.self, Never.self) viewModel.$lastQuery.subscribe(subscriber)スケジューラーを起動すると、テスト用のsubscriberがイベントを受け取り、TestSequenceとして記録してくれます。
// スケジューラを起動 scheduler.resume() let expected: TestSequence<String?, Never> = [ (000, .subscription), (000, .input(nil)), (100, .input("Swift")), (200, .input("Python")), ] // 受け取ったイベントと期待されるイベントを比較 XCTAssertEqual(subscriber.recordedOutput, expected)おわりに
意外と簡単にユニットテストが書けたのではないでしょうか?
どんどんテストを書いていきましょう!参考
UIViewControllerをXCTestでUnitテストする
Who said we cannot unit test SwiftUI views?
ViewInspector
Entwine
階層化されたSwiftUIのViewから、どうやって中身を取り出していくかという苦労が綴られていて面白いです。 ↩
- 投稿日:2020-08-20T17:53:45+09:00
JenkinsでiOSビルドする際に、プロビジョニングファイルを指定する
はじめに
Jenkinsで開発版をビルドしていて、いざAppStore向けにもビルドするとなったとき、プロビジョニングプロファイルを新しく作りました。
開発版ビルドはプロビジョニングプロファイルを自動指定だったので、このままだと開発版ビルドがどのプロビジョニングプロファイルを使うか分からないので明示的に指定する必要が出てきました。
秒で終わる内容ですが、その時の対応を備忘録としてメモしておきます。
環境
MacBook Pro(16-inch,2019)
macOS Catalina 10.15.2
XCode 11.3Jenkinsジョブ設定済みで、バッチビルドのセットアップは済んでいる状態とします。
ビルドしたのはUnityで作ったプロジェクトです。対応
ビルド時に使うplistにプロビジョニングプロファイルのIDを指定する
plistの内に以下の設定を追加します。
exportOption.plist<key>provisioningProfiles</key> <dict> <key>プロビジョニングファイルに紐付いたBundleID(ex:com.xxx.xxx)</key> <string>プロビジョニングファイルのName(ex:MyApp_Distribution)</string> </dict>XCodeにプロビジョニングプロファイルをダウンロードする
XCodeを開いて、プロビジョニングプロファイルをダウンロードします。
メニューバー→Xcode→Preferencesで設定を開きます。
次にAccountsタブで、指定したプロビジョニングプロファイルに含まれているチームやユーザを指定して「Download Manual Profiles」をクリックします。おわり
ダウンロードが終わるまで待てば完了です。
ジョブを動かして正しくビルドされるか確認します。補足
この設定が無い場合は、自動でプロビジョニングプロファイルを選択してビルドします(おそらくXCode内にダウンロードしてあり、ビルド対象プロジェクトで使えるものが選択される?)
XCodeのプロビジョニングプロファイルが古い時は、「~/Library/MobileDevice/Provisioning Profiles/」ディレクトリの中のプロビジョニングプロファイルを削除してから、上記のダウンロード操作をします。
参考リンク
https://qiita.com/taisuke_h/items/e37d96c96a811b630c0c
plistの各種設定の確認に。
https://qiita.com/roworks/items/7ef12acabf9679561d84
plistのxcodeビルド周り全般の参考に。
- 投稿日:2020-08-20T13:30:33+09:00
長押し続けている間繰り返し処理をするLongPressGestureRecognizer
UILongPressGestureRecognizer + Timerで
押し続けている間、連続で処理をするボタンをつくりたい。
たとえば、押している間ミサイルを連写するとか。
UILongPressGestureRecognizerの state.beganでタイマーのリピート処理を始め、 state.endedで Timer.invalidate( ) することで実現できます。@objc func repeateWhilePressing(_ gestureRecognizer : UILongPressGestureRecognizer){ switch gestureRecognizer.state { case .began: print("shot!")//短いタップでも反応するようにここにも処理を書いておきます。 pressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (Timer) in print("shot!")//押している間0.1秒間隔で繰り返し。 }) case .ended: pressTimer?.invalidate() default: break } }短いタップの場合も反応するように、Long Pressの最小プレス時間を小さく設定。
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(repeateWhilePressing())) longPress.minimumPressDuration = 0.01押している間の結果
shot!
shot!
shot!
shot!
shot!
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-08-20T12:18:19+09:00
(Swift 5対応)UILabelの文字に縁取り(袋文字)をする2つの方法
どうも、肩と背中の日焼けが激しすぎて服を着ないまま禁断の在宅勤務を行なっているiOSエンジニアのkaです。
文字の表示を簡単にネイティブの機能だけを使ってテレビ番組のテロップ風にしたいということでやってみました。
NSAttributedStringを使う方法
swift.swiftlet stroke:UIColor = UIColor.black let foreground:UIColor = UIColor.white let width:CGFloat = 3.0 let strokeTextAttributes = [ .strokeColor : stroke, .foregroundColor : foreground, .strokeWidth : width, .font : UIFont.boldSystemFont(ofSize: 20.0) ] as [NSAttributedString.Key : Any] let string = NSMutableAttributedString.init(string: "縁取り文字", attributes: strokeTextAttributes) label.attributedText = stringCGContextを使う方法
UILabelのサブクラスを作ります
DecoreateLabel.swiftclass DecorateLabel: UILabel { var strokeColor:UIColor = UIColor.white var strokeSize:CGFloat = 2.0 override func drawText(in rect: CGRect) { if let context = UIGraphicsGetCurrentContext() { let textColor = self.textColor context.setLineWidth(self.strokeSize) context.setLineJoin(CGLineJoin.round) context.setTextDrawingMode(.stroke) self.textColor = self.strokeColor super.drawText(in: rect) context.setTextDrawingMode(.fill) self.textColor = textColor } super.drawText(in: rect) } }swift.swiftlet label = DecorateLabel() label.frame = CGRect(x: 150, y: 200, width: 200, height: 20) let stroke:UIColor = UIColor.black let foreground:UIColor = UIColor.white let width:CGFloat = 8.0 label.strokeColor = stroke label.textColor = foreground label.strokeSize = width label.text = "縁取り文字"補足
NSAttributedStringを使う方法だと文字の内側に縁取り
CGContextを使う方法だと文字の外側に縁取りをします
場面によって使い分けましょう!参考:
storyboard で UILabel を袋文字にする
UILabelの文字に枠をつけ、袋文字(縁取り文字)を作る
UILabelをNSAttributedStringで文字装飾(Swift 4対応)終わりです
Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp
- 投稿日:2020-08-20T12:01:34+09:00
COCOA PODSでAlamofireとSwiftyJSONをインストールし、API通信。
Alamofireと、
URLSessionの比較についてメモ書き。
SwiftyJSONも。はじめに
COCOA PODS?
iOS開発での、パッケージ管理システム。
javascriptでいう、npmやyarnAlamofire?
iOS開発での、定番のAPIクライアント。
iOSのネットワーク通信処理をシンプルにする目的。SwiftyJSON?
Alamofireで取得したデータを、JSON形式で扱う。
インストール手順
【Swift】COCOAPODSでAlamofireとSwiftyJSONをインストールし、API通信をする方法
コード比較 (Webリクエスト)
Alamofireの、良いところ。
(data, response, error)が、(response)に集約do-catchが不要JSONSerialization.jsonObjectが不要task.resume()が不要URLSession
func getRandomPersonUrlSession(id: Int, completion: @escaping PersonResponseCompletion) { guard let url = URL(string: "\(PERSON_URL)\(id)") else { return } let task = URLSession.shared.dataTask(with: url) { (data, response, error) in guard error == nil else { debugPrint(error.debugDescription) return } guard let data = data else { return } do { let json = try JSONSerialization.jsonObject(with: data, options: []) guard let jsonAny = json as? [String: Any] else { return } let person = self.parsePersonManual(json: jsonAny) DispatchQueue.main.async { completion(person) } } catch { debugPrint(error.localizedDescription) return } } task.resume() }Alamofire
func getRandomPersonAlamo(id: Int, completion: @escaping PersonResponseCompletion) { guard let url = URL(string: "\(PERSON_URL)\(id)") else { return } Alamofire.request(url).responseJSON { (response) in if let error = response.result.error { debugPrint(error.localizedDescription) completion(nil) return } guard let json = response.result.value as? [String: Any] else { return completion(nil) } let person = self.parsePersonManual(json: json) completion(person) } }Alamofireを使うと、遥かに簡潔で見栄えが良くなりました。
コード比較 (JSON解析)
SwiftyJSONの、いいところ
as? [String: Any]が不要。(=キャストが不要)ライブラリなし。
// Persing function using manual methods private func parsePersonManual(json: [String: Any]) -> Person { let name = json["name"] as? String ?? "" let height = json["height"] as? String ?? "" let mass = json["mass"] as? String ?? "" let hair = json["hair_color"] as? String ?? "" let birthYear = json["birth_year"] as? String ?? "" let gender = json["gender"] as? String ?? "" let homeWorldUrl = json["homeworld"] as? String ?? "" let filmUrls = json["films"] as? String ?? "" let vehicleUrls = json["vehicles"] as? [String] ?? [String]() let starshipUrls = json["starships"] as? [String] ?? [String]() return Person(name: name, height: height, mass: mass, hair: hair, birthYear: birthYear, gender: gender, homeWorldUrl: homeWorldUrl, filmUrls: filmUrls, vehicleUrls: vehicleUrls, starshipUrls: starshipUrls) }SwiftyJSON
// Parsing with SwiftyJSON private func parsePersonSwifty(json: JSON) -> Person { let name = json["name"].stringValue let height = json["height"].stringValue let mass = json["mass"].stringValue let hair = json["hair_color"].stringValue let birthYear = json["birth_year"].stringValue let gender = json["gender"].stringValue let homeWorldUrl = json["homeworld"].stringValue let filmUrls = json["films"].stringValue let vehicleUrls = json["vehicles"].arrayValue.map({$0.stringValue}) let starshipUrls = json["starships"].arrayValue.map({$0.stringValue}) return Person(name: name, height: height, mass: mass, hair: hair, birthYear: birthYear, gender: gender, homeWorldUrl: homeWorldUrl, filmUrls: filmUrls, vehicleUrls: vehicleUrls, starshipUrls: starshipUrls) }記述量はあまり変わらないけど、大量の
as? [String: Any]が不要。
配列ではmapを使う。おしまい。
何かあれば、追記します。



