20211025のSwiftに関する記事は6件です。

配列で使える.sort()が便利だった

今回の内容 .sort()が便利だったので、その時のことを書きます。 コードと簡単解説 数日前に書いた連番判定するコードを.sort()を使うことで短くできました。 .sort()は配列内の値を並び替えることが出来ます。 例:["4","3","2","1"]を["1","2","3","4"]に並び替えることが出来ます。 .sort( )使用前のコード func judge(judgeTarget:String){ textContents = [] for text in judgeTarget{ textContents.append(String(text)) } if textContents[0] == textContents[1] && textContents[0] == textContents[2] && textContents[0] == textContents[3]{ alert() }else{ switch Int(textContents[0])! < Int(textContents[3])!{ case true: if Int(textContents[3])! - Int(textContents[0])! == 3 && (Int(textContents[1])! - Int(textContents[0])!) + (Int(textContents[3])! - Int(textContents[2])!) == 2{ alert() } case false: if Int(textContents[0])! - Int(textContents[3])! == 3 && (Int(textContents[0])! - Int(textContents[1])!) + (Int(textContents[2])! - Int(textContents[3])! ) == 2{ alert() } } } } .sort( )使用後のコード 以前は、["4","3","2","1"]か["1","2","3","4"]なのかを判定してましたが、.sort()を使用することで["1","2","3","4"]か["1","1","1","1"]の様な場合だけ判定すれば良くなりました。 func judge(judgeTarget:String){ textContents = [] for text in judgeTarget{ textContents.append(String(text)) textContents.sort() } if textContents[0] == textContents[1] && textContents[0] == textContents[2] && textContents[0] == textContents[3]{ alert() }else if Int(textContents[3])! - Int(textContents[0])! == 3 && (Int(textContents[1])! - Int(textContents[0])!) + (Int(textContents[3])! - Int(textContents[2])!) == 2{ alert() } } 終わり ご指摘、ご質問などありましたら、コメントまでお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI + Combine で MVVM & Clean Architecture な設計を考えてみた

はじめに こんにちは。 iOS 13 で SwiftUI がリリースされてから早 2 年。 そろそろ SwiftUI をちゃんと学んでおこうと思い立ち、趣味プロダクトで SwiftUI の習作アプリを開発してみました。 出来上がったもの 小説投稿サイト 小説家になろう の公開 API を利用させていただき、該当サイトの閲覧アプリを開発しました。 動作イメージ 実装した機能 「日間」「週間」「月間」「四半期」の小説ランキング閲覧機能 小説の検索機能 小説の閲覧機能(ただの WebView ですが。。。) 実際のコード 以下のリポジトリに置いてあります。もし興味がありましたら是非触ってみてください。 https://github.com/inokinn/NaroViewer アーキテクチャについて SwiftUI を使ってみるにあたり、まず悩んだのはアーキテクチャの選定でした。 SwiftUI は Combine との相性が良く、折角なので Combine によるデータバインディングが活きる MVVM でやってみようと考えました。 とはいえ、 MVVM は所謂 GUI アーキテクチャであり、プレゼンテーションロジックとドメインロジックの切り離し以外は興味の対象外です。そこで、データの永続化や API へのアクセス周りに関しても総合的に勘案の上、 MVVM ベースの Clean Architecture について考えてみることにしました。 検討した結果のアーキテクチャ こんな感じになりました。 アーキテクチャのルール レイヤー構造は、 Domain 、 Presentation 、 Infrastructure の 3 層構成なのですが、 Clean Architecture の有名な同心円状の図のレイヤー構造にも準拠しています。 ( Entity 、 UseCase 、 Interface Adapter 、 Framework & Driver の 4 層。) この図の上から順に上位のレイヤーであることを表します(スペースの都合で UseCase と Entity が並んでいますが、 Entity は最上位レイヤーです)。 この 2 通りのレイヤー構造のどちらにおいても、依存の方向は 下位レイヤー -> 上位レイヤー であることを徹底しています。 上位レイヤーが下位レイヤーにアクセスする際には、必ず実装ではなくプロトコルに依存することによって、下位レイヤーに直接依存することを避けています(依存性逆転の原則)。 上位レイヤーのモジュールから下位レイヤーの参照を保持する場合、 Swinject を用いて依存性注入を行っています。 データの流れ データの取得には Combine によるデータバインディングを用いています。 例として、API 通信を行って小説のランキングデータを取得する際の、データの流れは下図のようになります。 この部分について、ソースコードを抜粋したものが以下になります。 View から ViewModel のメソッドを呼び出す RankingView.swift import SwiftUI import Combine struct RankingView: View { @ObservedObject var viewModel: RankingViewModel @State var rankingType = Ranking.RankingType.Daily var body: some View { ZStack { NavigationView { (略) } .navigationViewStyle(StackNavigationViewStyle()) .onAppear { if viewModel.ranking.rowList.count == 0 { self.loadRanking() } } (略) } } func loadRanking() { viewModel.fetchRanking(type: self.rankingType) } ここでは、画面が表示された際に ViewModel のメソッド呼び出しを行っています。 ViewModel にて、 UseCase の処理を購読する RankingViewModel.swift import SwiftUI import Combine final class RankingViewModel: ObservableObject, Identifiable { @Published var ranking: Ranking = Ranking(rowList: []) @Published var loading: Bool = false private var disposables = Set<AnyCancellable>() // ランキングの取得 func fetchRanking(type: Ranking.RankingType) { self.loading = true self.ranking = Ranking(rowList: []) AppBuilder.shared.rankingUseCase.startFetch(type: type) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in self.loading = false switch completion { case .finished: break case .failure(_): // TODO: エラーハンドリングちゃんとやる break } }, receiveValue: { [weak self] ranking in self?.ranking = ranking }) .store(in: &disposables) } } ViewModel では UseCase の処理を購読し、結果を受け取って値を保持したりエラー処理を行っています(サンプルコードではエラー処理をちゃんとやってないけど。。。)。 UseCase でアプリケーション固有のロジックを書く RankingUseCase.swift final class RankingUseCase: RankingUseCaseProtocol { let rankingGateway: RankingGatewayProtocol let rankingRowsGateway: RankingRowsGatewayProtocol init(rankingGateway: RankingGatewayProtocol, rankingRowsGateway: RankingRowsGatewayProtocol) { self.rankingGateway = rankingGateway self.rankingRowsGateway = rankingRowsGateway } func startFetch(type: Ranking.RankingType) -> AnyPublisher<Ranking, Error> { return rankingGateway .fetch(type: type) .flatMap { [weak self] ranking -> AnyPublisher<Ranking, Error> in return (self?.rankingRowsGateway.fetch(ranking: ranking))! } .eraseToAnyPublisher() } } UseCase です。 なろう API の仕様として、ランキング情報取得 API のレスポンスには、小説の識別子データはあるものの、実際の小説データ(タイトルや作者名など)は含まれませんので、ランキング情報を元に小説情報をリクエストする必要があります。 そこで、今回は Combine のオペレーターの一つである flatMap を用いて、ランキング情報を持つ Publisher を、小説情報を取得する Publisher に変換し、 2 つの API アクセスを直列で実行しています。 このように、一連の処理の流れを宣言的に記述出来るのも Combine の利点です。 Gateway は下位レイヤーとドメインの橋渡しをする RankingGateway.swift final class RankingGateway: RankingGatewayProtocol { let apiClient: RankingAPIClientProtocol init(apiClient: RankingAPIClientProtocol) { self.apiClient = apiClient } func fetch(type: Ranking.RankingType) -> AnyPublisher<Ranking, Error> { // type をパラメータの rtype に変換 var rtype = "" let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyyMMdd" dateFormatter.locale = Locale(identifier: "ja_JP") let weekDayFormatter = DateFormatter() weekDayFormatter.dateFormat = "EEEEE" weekDayFormatter.locale = Locale(identifier: "ja_JP") // 月初を取得 var components = Calendar.current.dateComponents([.year, .month, .day],from: Date()) components.day = 1 let firstDay = Calendar.current.date(from: components)! switch type { case .Daily: // "yyyyMMdd-d" の形に変換 rtype = dateFormatter.string(from: Calendar.current.date(byAdding: .day, value: -1, to: Date())!) + "-d" break case .Weekly: // 火曜日を特定した後、 "yyyyMMdd-w" の形に変換 var targerDay = Date() while weekDayFormatter.string(from: targerDay) != "火" { targerDay = Calendar.current.date(byAdding: .day, value: -1, to: targerDay)! } rtype = dateFormatter.string(from: targerDay) + "-w" break case .Monthly: // 月初を特定した後、 "yyyyMMdd-m" の形に変換 rtype = dateFormatter.string(from: firstDay) + "-m" break case .Quarter: // 月初を特定した後、 "yyyyMMdd-q" の形に変換 rtype = dateFormatter.string(from: firstDay) + "-q" break } // レスポンスを Entity に変換し UseCase に返す return apiClient.fetch(rtype: rtype) .tryMap { response -> Ranking in var rankingRows: [RankingRow] = [] for row in response { rankingRows.append(RankingRow(ncode: row.ncode, pt: row.pt, rank: row.rank, novel: nil)) } return Ranking(rowList: rankingRows) } .eraseToAnyPublisher() } } ランキングの取得方式は「日間」「週間」「月間」「四半期」を選択することが可能です。 この API のリクエストパラメータには、多くのルールが存在します。 日間ランキングは、朝になるまでデータが生成されない(夜中に本日の日時を指定するとエラーになる) 週間ランキングなら集計の都合上、火曜日の日付を指定する必要がある 月間および四半期ランキングの場合、対象月の1日(20211001 など)を指定する必要がある リクエストパラメータを作成する際には、これらを念頭に置かなければなりません。しかし、これは API の都合であり、アプリの仕様や要件には直接関係の無いものです。したがって、 Gateway は入力としては「ランキングのタイプは月間としてデータを取得したい」など、 API の仕様とは直接関係ないパラメータを受け取って API が必要とするパラメータに変換し、実際に API リクエストを行う最下レイヤーとの橋渡し役を担います。 また、後に受け取ったレスポンスを Entity のデータ構造に変換する役割も担っています。 APIClient でリクエストの生成を行う RankingAPIClient.swift import Alamofire import Combine final class RankingAPIClient: RankingAPIClientProtocol { func fetch(rtype: String) -> AnyPublisher<[RankingResponse], Error> { let request = RankingRequest() request.rtype = rtype request.out = "json" return APIAccessPublisher.publish(request).eraseToAnyPublisher() } } APIAccessPublisher が実際の API アクセスを行う APIAccessPublisher.swift import Alamofire import Combine import Foundation struct APIAccessPublisher { private static let contentType = "application/json" private static let decoder: JSONDecoder = { let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase return jsonDecoder }() static func publish<T, V>(_ request: T) -> Future<V, Error> where T: BaseRequest, V: Codable, T.ResponseType == V { return Future { promise in let api = AF .request(request) .responseJSON { response in switch response.result { case .success: do { if let data = response.data { let json = try self.decoder.decode(V.self, from: data) promise(.success(json)) } else { promise(.failure(AFError.responseValidationFailed(reason: .dataFileNil))) } } catch { promise(.failure(AFError.responseValidationFailed(reason: .dataFileNil))) } case .failure: promise(.failure(AFError.responseValidationFailed(reason: .dataFileNil))) } } api.resume() } } } APIAccessPublisher は、 Alamofire を用いて実際に API 通信を行います。 レスポンスのデータ構造を定義しておき、 json をそのデータ構造に変換します。 結果は promise() を用いて Publish します。 機能追加のために新規で API を作成する場合でも、実際に通信を行うのはこのクラスになります。 ちなみに、レスポンスデータは Entity とデータ構造が類似しており、使いまわして直接 Entity に格納したいと感じることもありますが、これらのデータ構造の目的は全く異なり、それぞれ違う理由で変更が入るため、単一責任の原則に反するため、使い回さない方がよいとされています。 ところで、先述のとおり、この例では直列でもう一つ API を叩くのですが、もうひとつの APIClient についてはここでは割愛します。 依存性の注入 アーキテクチャのルール で述べた通り、上位レイヤーが下位レイヤーにアクセスする際には、必ず実装ではなくプロトコルに依存することによって、下位レイヤーに直接依存することを避けています。 そこで、上位レイヤーのモジュールから下位レイヤーの参照を保持するために、 Swinject を用いて下記のように依存性の注入(DI)を行っています。 AppBuilder.swift import Swinject final class AppBuilder { static let shared = AppBuilder() let rankingUseCase: RankingUseCaseProtocol let searchNovelUseCase: SearchNovelUseCaseProtocol // DI コンテナに Service を登録 let swinjectContainer = Container() { c in // フレームワーク・ドライバ c.register(RankingAPIClientProtocol.self) { _ in RankingAPIClient() } c.register(RankingRowsAPIClientProtocol.self) { _ in RankingRowsAPIClient() } c.register(SearchNovelAPIClientProtocol.self) { _ in SearchNovelAPIClient() } // インターフェイスアダプター c.register(RankingGatewayProtocol.self) { r in RankingGateway(apiClient: r.resolve(RankingAPIClientProtocol.self)!) } c.register(RankingRowsGatewayProtocol.self) { r in RankingRowsGateway(apiClient: r.resolve(RankingRowsAPIClientProtocol.self)!) } c.register(SearchNovelGatewayProtocol.self) { r in SearchNovelGateway(apiClient: r.resolve(SearchNovelAPIClientProtocol.self)!) } // ユースケース c.register(RankingUseCaseProtocol.self) { r in RankingUseCase(rankingGateway: r.resolve(RankingGatewayProtocol.self)!, rankingRowsGateway: r.resolve(RankingRowsGatewayProtocol.self)!) } c.register(SearchNovelUseCaseProtocol.self) { r in SearchNovelUseCase(searchNovelGateway: r.resolve(SearchNovelGatewayProtocol.self)!) } } private init() { // ユースケースの作成 self.rankingUseCase = self.swinjectContainer.resolve(RankingUseCaseProtocol.self)! self.searchNovelUseCase = self.swinjectContainer.resolve(SearchNovelUseCaseProtocol.self)! } } おわりに 今回は、 SwiftUI + Combine (と Swinject) で MVVM & Clean Architecture を構築し、アプリを作成してみました。 とはいえ SwiftUI ならではという話は結果的に皆無になってしまったので、 UIKit でも同じような感じになると思います。 Combine を初めて使った際、データの伝播のさせ方や直列による処理の書き方などがあまり分からず、やや手探りでの実装となりましたが、何となく慣れてきた今はこのように宣言的に記述出来るのは楽でよいなと感じています。 SwiftUI は現時点でまだまだ UIKit で出来ていたことには及ばず、不便を感じることが多いですが、既に新規プロダクトでは SwiftUI で開発しているという事例もいくつか耳にするようになったため、引き続き SwiftUI でやっていけるアーキテクチャを模索したいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】おしゃれを導入!デザイン系ライブラリ

はじめに SwiftUIはAppleの標準アプリのような画面が実装できるので、デザインに関しては素人でも大失敗はしないという安心感があります。ただちょっと一工夫するのが意外と苦労したりします。今回はSwiftUIにおいて使えるデザイン系ライブラリのライブラリを紹介したいと思います。 紹介するライブラリは全てSwift Package Managerによる導入が可能です。 Liquid 泡のような図形を表示できるライブラリです。アニメーションが含まれているので、ゆっくり動くのが特徴です。色をつけて背景にすることもできますし、写真のフレームのような使い方もできます。 画像はライブラリの用意したView、Liquidを3つ重ねて作っていますが、泡のような感じを出すために重ねて使うことが多いかと思います。ひとつひとつ設定を変えることもできるので、一番手前だけ色を変えるなど細かい調節が可能です。 Colorful 画像のようなカラフルなViewを作成することができます。色やアニメーションの可否なども設定できます。色は複数指定できますし、逆に単色にして霧のような表現をすることも可能です。 基本的には背景として使うことになりそうですが、以前紹介したグラスモーフィズムとも相性がよいかもしれません。 StatefulTabView Statefulの名前の通り、状態によってTabの表示を変更できるようにすることが主な使い方です。例えば、アイコンに数字や文字を表示して通知などを知らせることができます。 これだけでも便利なのですが、実はこのライブラリを使うとTab部分の色を細かく変更することができます。 SwiftClockUI アナログ時計を表示するもので、文字盤は4種類用意されています。時計の色ももちろん変えられますし、アニメーションもなめらかです。 もともと画面に時間表示のあるiOSだとあまり出番がありませんが、MacOSのアプリだと活用の場面は増えてきます。例えば、ToDoアプリなどではアナログ表示で時間が見られると便利かもしれません。 MarkDownUI その名の通りマークダウンで文章を扱えるようになります。長文でもオフラインで表示したい規約や使い方などはこれを使うのがいいかもしれません。URL部分の色の変更など、細かい調整も可能です。 Textそのままよりもバランスが取りやすくなるのは非常に便利だと思います。マークダウン記法は広く普及しているので、サイトや文書を構成の参考にできるのが大きなメリットではないでしょうか。 記法としてはCommonMark Specに完全に準拠しているそうです。 一行目に日本語のみを見出し表示すると文字の上半分が途切れてしまうことがあります。対策としては以下のように空のHeaderを挿入してください。 # # # 見出し1 本文 DynamicColor UIColorとColorに便利な機能を追加できるものです。まずUIColorには色の調整を自動で行ってくれる機能が追加されます。下の画像のように基本の色に対して濃淡を変えたり、対抗色を生成したりすることが可能です。 SwiftUIのColorでもhex指定で色を決定できるようになるので、ちょっとした色の微調整も簡単になります。 さいごに SwiftUIに対応したライブラリもかなり増えてきたので今回はデザイン系に絞って紹介してきました。全てSwift Package Managerで簡単に管理できるのでとりあえず試してみるのも良いかと思います。ぜひ開発に役立ててください! よかったらこちらもよろしくお願いします☺️ 各種お知らせ:Twitter 作成途中のアプリについて:note
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Firebaseでパスワード再設定メールを送信

Swiftを用いたiOS開発でFirebaseでパスワード再設定メールを送信するコード。 import Firebase Auth.auth().sendPasswordReset(withEmail: "example@example.com") { error in if error != nil { print("error") } else{ print("success") } } sendPasswordResetWithEmail:completion:メソッドを使用して、ユーザーにパスワードの再設定メールを送信している。 import Firebaseを忘れずに。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mapで位置情報を示し、ピンをたてる

はじめに 超初心者でも簡単にMap機能が使えるように、その概要をざっくり理解してとりあえず動くものを作るまでの記録? Map機能を使ったことがなく、これから位置情報を扱うアプリを作ってみたいと言う方向けに超基本をチュートリアルとしてまとめました。 地図に位置情報を表示 おまけ 指定した場所にピンを置く 地図機能についてまずは簡単におさらいします。それが終わったら、セットアップを始めましょう。 Map機能について知る Swiftで使えるMKMapViewについて学んでいきましょう。 MapKitとは アプリ内に地図または衛星画像を表示し、興味のある地点を位置情報として呼び出し、地図座標の目印情報を決定するSwiftのライブラリです。 セットアップ SwiftでMapKitを使う際には以下の2点が必要になります。 1. info.plistを選択 2.+を押して、Privacy-Location when in Use Usage Descriptionを選択し、そのよこのValueの欄に「位置情報を使用します」(文言はなんでもいい)と入力します。 3.Privacy-Location Always and When in Use Usage Descriptionも同様に選択し、同じ文言を書きます。 こうすることで、ユーザーに位置情報の使用を求める準備ができる Storyboardの準備 では、早速始めてみましょう。 ViewControllerにMapKitViewをのせ、四方を固定してオートレイアウトします。 これでStoryboardの準備は完了です。 ユーザーに位置情報の使用の確認を促す ユーザーに位置情報表示の許可を取り、取れなかった場合は許可をリクエストし、とれた場合は位置情報の表示を開始します。 private func configureLocationServices() { //セキュリティ認証のステータス let status = CLLocationManager.authorizationStatus() //ユーザーの許可が取れなかった場合 if status == .notDetermined { //許可をリクエスト locationManager.requestAlwaysAuthorization() //ユーザーの許可が取れた場合 } else if status == .authorizedAlways || status == .authorizedWhenInUse {        //ユーザーの位置情報の表示を開始 beginLocationUpdates(locationManager: locationManager) } } ユーザーの位置情報の表示を開始 地図の精度と現在地をユーザーに報告する更新の生成を開始します。 private func beginLocationUpdates(locationManager: CLLocationManager) { //マップがユーザーの場所を表示する mapView.showsUserLocation = true //アプリが受信したいレベルの精度を利用可能な最高レベルに設定 locationManager.desiredAccuracy = kCLLocationAccuracyBest //ユーザーの現在地を報告する更新の生成を開始します。 locationManager.startUpdatingLocation() } 最新の位置情報の更新を取得して、ズームする アプリを開いて最初に位置情報を表示する際に、設定した範囲に地図をズームさせます。 //ユーザーが場所を変更するたびにアップデートされる(現在地の更新時に何かしたいとき) extension MapPointViewController:CLLocationManagerDelegate{ //新しい位置情報が利用可能になったことをdelegateに知らせる func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { print("最新の位置情報を取得") //latestLocationがnilの時を弾く guard let latestLocation = locations.first else { return } //もし、現在地がnilだったら(起動して最初の時) if currentCoordinate == nil { //設定した新しい領域の範囲までズームする zoomToLatestLocation(with: latestLocation.coordinate)        //ピンを置くときに呼ばれる(②) addAnnotation() } currentCoordinate = latestLocation.coordinate } 位置情報の更新 ユーザーの位置情報の変化を更新して、表示します。 ターミナルに場所が変更されたかどうかを表示するためにprint文を入れます。 //アプリがlocationManagerを作成したとき、および承認ステータスが変更されたときに、delegateに承認ステータスを通知します。 func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { print("場所が変更されました") //位置情報を常に許可している、またはアプリを使っている時だけ使用許可にしているとき if status == .authorizedAlways || status == .authorizedWhenInUse { //現在地の更新の表示を始める beginLocationUpdates(locationManager: manager) } } } おまけ 指定した場所にピンを立ててみよう geeksalon恵比寿校(35.64593086630865, 139.70462782640197)に照準を合わせて、ピンを立ててみましょう。 private func addAnnotation(){ //ピンを立てる let geeksalonAnnotation = MKPointAnnotation() //恵比寿教室の緯度軽度を入力 geeksalonAnnotation.coordinate = CLLocationCoordinate2DMake(35.64593086630865, 139.70462782640197) //ピンのタイトル geeksalonAnnotation.title = "geeksalon" //ピンのサブタイトル geeksalonAnnotation.subtitle = "素敵な人がいっぱい" //mapViewにピンを追加する mapView.addAnnotation(geeksalonAnnotation) } }  表示されました。 まとめ ①ユーザーに位置情報の使用許可をとる ②info.plistで位置情報設定をする ③ユーザーに位置情報の使用の確認を促す ④ユーザーの場所を表示する ⑤場所と縮尺を設定してズームする ⑥位置情報の更新 import UIKit import MapKit //位置情報は(少なくとも日本では)個人情報保護の観点から、ユーザー許可を取る必要がある。 class MapPointViewController: UIViewController { private let locationManager = CLLocationManager() //現在の緯度経度の指定 private var currentCoordinate: CLLocationCoordinate2D? @IBOutlet weak var mapView: MKMapView! override func viewDidLoad() { super.viewDidLoad() configureLocationServices() } private func configureLocationServices() { locationManager.delegate = self //セキュリティ認証のステータス let status = CLLocationManager.authorizationStatus() //ユーザーの許可が取れなかった場合 if status == .notDetermined { //許可をリクエスト locationManager.requestAlwaysAuthorization() //ユーザーの許可が取れた場合 } else if status == .authorizedAlways || status == .authorizedWhenInUse { beginLocationUpdates(locationManager: locationManager) } } private func beginLocationUpdates(locationManager: CLLocationManager) { //マップがユーザーの場所を表示する mapView.showsUserLocation = true //アプリが受信したいレベルの精度を利用可能な最高レベルに設定 locationManager.desiredAccuracy = kCLLocationAccuracyBest //ユーザーの現在地を報告する更新の生成を開始します。 locationManager.startUpdatingLocation() } private func zoomToLatestLocation(with coordinate: CLLocationCoordinate2D) { //場所と尺度を組み合わせてできるのがMKCoordinateRegion let zoomRegion = MKCoordinateRegion(center: coordinate, latitudinalMeters: 10000, longitudinalMeters: 10000) mapView.setRegion(zoomRegion, animated: true) } private func addAnnotation(){ //ピンを立てる let geeksalonAnnotation = MKPointAnnotation() //恵比寿教室の緯度軽度を入力 geeksalonAnnotation.coordinate = CLLocationCoordinate2DMake(35.64593086630865, 139.70462782640197) //ピンのタイトル geeksalonAnnotation.title = "geeksalon" //ピンのサブタイトル geeksalonAnnotation.subtitle = "素敵な人がいっぱい" //mapViewにピンを追加する mapView.addAnnotation(geeksalonAnnotation) } } extension MapPointViewController:CLLocationManagerDelegate{ //新しい位置情報が利用可能になったことをdelegateに知らせる func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { print("最新の位置情報を取得") //latestLocationがnilの時を弾く guard let latestLocation = locations.first else { return } //もし、現在地がnilだったら(起動して最初の時) if currentCoordinate == nil { //設定した新しい領域の範囲までズームする zoomToLatestLocation(with: latestLocation.coordinate) addAnnotation() } currentCoordinate = latestLocation.coordinate } //アプリがlocationManagerを作成したとき、および承認ステータスが変更されたときに、delegateに承認ステータスを通知します。 func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { print("場所が変更されました") //位置情報を常に許可している、またはアプリを使っている時だけ使用許可にしているとき if status == .authorizedAlways || status == .authorizedWhenInUse { //現在地の更新の表示を始める beginLocationUpdates(locationManager: manager) } } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Human Interface Guidelinesを理解する その2

ロードに関して コンテンツの読み込みに関して、何らかのわかりやすい動きを示す必要がある。Netflixのダウンロードや、音声がどれくらい流れているか?といったこと。ここら辺はコンテンツ系のアプリを使用してみると、どんな感じでコンテンツを利用しているかをみることができる。 ・ロード発生をしっかり伝えること ロード中のクルクルを出してしっかりロード発生を伝える。あわよくば、どれくらいの進捗かどうかも伝えれれば良い。 プログレスバーなどで表示できそう。Youtubeではスケルトンビューでローディングをしているのを思い出した。。 ・なるべく早くコンテンツを表示する。 ユーザーが操作している間にバックグランドでローディングをしておくなどの工夫が必要。個人的には動画とかでなければ1秒以上読み込みが起こると違和感があるなぁと感じる。 身近なものだとTiktokは動画をバックグラウンドでとってきて、ユーザーに読み込みをほとんど感じさせない設計になっているのかな?とてもUX体験が良い ・ロード中に代わりとなるコンテンツ(動画や音声、ゲームなど)を表示してロード中であることをユーザーに感じさせない。 あんまし身近なもの例が思いつかない。あまりゲームをしないからかも ・インジケーターや他のロードを示すコンポーネントをアプリの世界観にあったものにする。  モーダルに関して ・モ-ダルは、ユーザーのそれまでの文脈から離れ、一時的にコンテンツを提示するデザイン手法。 終了するには明示的なアクションが必要となってくる。 モーダルでコンテンツを提示することにより、 ユーザーは自己完結型のタスク,ある一つのタスクに関する一連のまとまった操作に集中できる ユーザーに重要な情報を示し、必要に応じて次の操作を誘導する モーダルの仕方 シート シートスタイルは、コンテンツを部分的に覆うカードとして表示される(ViewControllerの上にポンと出てくるやつ)、カバーされていないすべての領域は暗くすることで、ユーザーの操作を意図的に防ぐ。 親ビューまたは前のカードの上端は現在のカードの後ろに表示され、カードを開いたときに中断したタスクを思い出せるようにします。 複雑なタスクを行わず、非没入型のモーダルコンテンツにはシートを使用します。 フルスクリーン フルスクリーンスタイルは、画面全体をカバーする。 前のビューは完全にカバーされているため、混乱を抑えられる。 ユーザーはボタンをタップじしてフルスクリーンを閉じる フルスクリーンは、ビデオ、写真、カメラビュー等の没入型コンテンツや何かのデータの編集などの複雑な一連のタスクをする場合に使用する。 ユーザーデータの編集とかメールを送信するとかでよく使われているイメージ。 モーダルを使用する場合の注意点 ・合理的にモダリティを使用しましょう そもそもモーダルは、現在のタスクとは異なるタスクを選択したり実行したりすることにユーザーを集中させる必要がある場合のみ。 モーダルエクスペリエンスは、現在の状況からユーザーを引き離し、閉じるためのアクションを必要とするので、明確なメリットが得られる場合にのみ使用し、むやみやたらに使用するべきではない。 ・不可欠な(かつ実利的な)情報を提供するためにアラートを残しておく アラートは何かが間違っている場合に表示する。むやみやたらに使用するものではない。そこに正当性を持たせることが必要。 ・モーダルタスクはシンプルで短く、焦点を絞りましょう モーダルタスクが多かったり、モーダルを階層構造で多くなると元の画面に戻ることが難しくなり、「そもそもなんのタスクでこの画面をモーダルさせったんだっけ?」となるのでやめた方が良い。 ・常にモーダルビューを閉じるボタンをつける。 完了またはキャンセルのボタンを絶対つけたほうが良い。 ・必要に応じて、モーダルビューを閉じる前に確認し、データの損失を回避する メールの下書きとかをイメージするばわかりやすい。モーダルを閉じる前に「データがなくなっちゃうよ!!」と言ってあげる。 ・ポップオーバーの上にカードを表示しない ポップオーバーは簡単な補足や情報を表示したりするのに便利だが、複雑なものは混乱させる ・一般に、モーダルタスクを識別するタイトルを表示しましょう モーダルタスクに入ると、以前のタスクから切り替わるので、新しいコンテキストを明確に示す必要がある。 ・モーダルビューの外観をアプリと整合性を持たせる ナビゲーションコントローラーにモーダルしてそれが明示的にわかってないとユーザーが混乱してしまうよ。 ・アプリ内で一貫したモーダルの遷移スタイルを選択する モーダルを表示させる方法はアプリ内で一つに限定した方が良い。下から上が一般的だが、上から下へしてみたり、ハーフモーダルが多かったりすると混乱する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む