- 投稿日:2020-04-21T23:35:01+09:00
Swiftでapiを使って天気予報を取得しよう
はじめに
Swift初心者が学習を兼ねて、作ってみました。
環境
Swift 5.1.2
Xcode 11.2使用したお天気api
http://weather.livedoor.com/weather_hacks/webservicePodをインストールする
Podfilepod "Alamofire" pod "SwiftyJSON"この2つのライブラリをインストールする事で、
簡単にHTTP通信と、JSONを扱うことができます。Main.storyboardを作る
こんな感じでパーツを配置します。
今回はpickerViewを使用して、地域を選択できるようにします。
あとはラベルとボタンを、いい感じに配置します。
ボタンを押すと、選択した地域の天気予報を取得し、次の画面でラベルに反映するようにします。
とてもシンプルです。また、今回はNavigation Controllerを使用しています。
タブのEditor -> Embed In -> Navigation Controller
で使用できます。実装する
まずは、pickerViewに入れるデータをmodelに書いておきます。
CityModel.swiftimport Foundation class CityModel { //地域の名前とURL追加するプロパティを持ちます。 let code: String let name: String init(cityCode:String, cityName:String) { code = cityCode name = cityName } }CityList.swiftimport Foundation class CityList { var list = [CityModel]() //Webサイトを見て適当に地域を追加します。 init() { list.append(CityModel(cityCode: "016010", cityName: "札幌")) list.append(CityModel(cityCode: "015010", cityName: "室蘭")) list.append(CityModel(cityCode: "040010", cityName: "仙台")) list.append(CityModel(cityCode: "110010", cityName: "さいたま")) list.append(CityModel(cityCode: "130010", cityName: "東京")) list.append(CityModel(cityCode: "140010", cityName: "横浜")) } }次に、最初の画面の処理を書いていきます。
この画面では、選択したデータを次の画面に渡すだけです。ViewController.swiftimport UIKit //pickerを使うので、DelegateとDataSourceを追加します。 class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var pickerView: UIPickerView! @IBOutlet weak var resultButton: UIButton! //次のVCに渡す変数に初期値を入れておきます。 var cityData:[String] = ["016010", "札幌"] //modelのデータを持ってきます。 let cityList = CityList() override func viewDidLoad() { super.viewDidLoad() //pickerを使うので、デリゲートを設定します。 pickerView.delegate = self pickerView.dataSource = self } //pickerの列の数を決めます。 func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } //pickerの行数を決めます。 func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return cityList.list.count } //pickerに表示するデータを決めます。 //ここでは取得したmodelのデータのnameを表示します。 func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return cityList.list[row].name } //選択時の挙動を決めます。 //次の画面に渡すために、取得したmodelのデータから、codeとnameを変数に入れます。 func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { cityData = [cityList.list[row].code,cityList.list[row].name] } //ボタン押した時の処理を書きます。画面遷移です。 @IBAction func resultButton(_ sender: Any) { performSegue(withIdentifier: "result", sender: nil) } //次の画面にデータを渡す処理を書きます。 //prepareは、segueが動作するとViewControllerに通知してくれます。 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "result" { //resultViewController(次の画面)で作った変数に、pickerで選択した地域の情報を入れます。 let resultVC = segue.destination as! ResultViewController resultVC.cityData = cityData } } }次の画面を実装します。
この画面では、天気予報の取得とラベルに反映を行います。resultViewContoller.swiftimport UIKit //Podからインスールしたライブラリを使うのでインポートします。 import Alamofire import SwiftyJSON //componentsを使用するのでインポートします。 import Foundation class ResultViewController: UIViewController { @IBOutlet weak var cityNameLabel: UILabel! @IBOutlet weak var detailsTextView: UITextView! @IBOutlet weak var todayDateLabel: UILabel! @IBOutlet weak var todayWeatherLabel: UILabel! @IBOutlet weak var todayHighLabel: UILabel! @IBOutlet weak var todayLowLabel: UILabel! @IBOutlet weak var tomorrowDateLabel: UILabel! @IBOutlet weak var tomorrowWeatherLabel: UILabel! @IBOutlet weak var tomorrowHighLabel: UILabel! @IBOutlet weak var tomorrowLowLabel: UILabel! let highString = "最高気温:" let lowString = "最低気温:" //取得した天気予報のデータを入れます。 var date:String = "" var weather:String = "" var high:String = "" var low:String = "" var details:[String] = [] //ViewController(前の画面)に渡し、選択した地域のデータを入れます。 var cityData:[String] = [] override func viewDidLoad() { super.viewDidLoad() getWeatherData(row: 0) getWeatherData(row: 1) } //天気情報を取得する。 private func getWeatherData(row: Int) { //前の画面から渡されたデータのcodeを、urlに組み込みます。 let url = "http://weather.livedoor.com/forecast/webservice/json/v1?city=\(cityData[0])" //Alamofireで通信します。データを取得します。 AF.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON {(response) in switch response.result { case .success: //jsonを取得します。 let json:JSON = JSON(response.data as Any) //取得したjsonから、必要なデータを取り出します。 let date = json["forecasts"][row]["date"].string let weather = json["forecasts"][row]["telop"].string let high = json["forecasts"][row]["temperature"]["max"]["celsius"].string let low = json["forecasts"][row]["temperature"]["min"]["celsius"].string let details = json["description"]["text"].string //取り出したデータを、それぞれの変数に入れます。 //データがない場合もあるので、その時は"No Data"と入れておきます。 if date != nil { self.date = String(date!.suffix(5)) //2025-04-10 -> 04-10 } else { self.date = "No Data" } if weather != nil { self.weather = weather! } else { self.weather = "No Data" } if high != nil { self.high = "\(high!)°" } else { self.high = "No Data" } if low != nil { self.low = "\(low!)°" } else { self.low = "No Data" } if details != nil { self.details = (details?.components(separatedBy: .newlines))! //改行で分割します。 } else { self.details = ["No Details"] } //ラベルに反映させます。 self.setWeatherData(row: row) case .failure(let error): print("-------- エラー ------") print(error) } } } //ラベルに反映させる処理です。 //引数を与えて、todayとtomorrowで分岐させます。 private func setWeatherData(row: Int) { if row == 0 { todayWeatherLabel.text = weather todayDateLabel.text = date todayHighLabel.text = highString + high todayLowLabel.text = lowString + low //分割時に配列になっているので、1つ目の要素だけ表示します。 detailsTextView.text = details[0] } else if row == 1 { tomorrowWeatherLabel.text = weather tomorrowDateLabel.text = date tomorrowHighLabel.text = highString + high tomorrowLowLabel.text = lowString + low } } }完成
コードの内容はさておき、天気予報のデータの取得に成功しました。
ViewDidLoad内でフォントサイズやボタンの枠線を変更していますが、
長くなるので割愛しました。お好みでどうぞ。
- 投稿日:2020-04-21T20:32:03+09:00
Xcodeのシミュレーターでダークモードに切り替えるキーボードショートカット
初めに
Xcodeのシミュレーターでダークモードを使える機能が実装されていましたが、
意外にもショートカットを使った記事が無かったので備忘録がわりに掲載します。環境
Xcode11.4
(注:Xcode11.3では実行できなかった)
参考記事
iOS シミュレーターでダークモードを切り替える2つの方法(画像付き)
https://qiita.com/zono_Bianchi/items/e79ddc6b6bc52d854fcf操作方法
Command(⌘) + Shift(⇧) + A
これだけで簡単にシミュレーターでダークモードへの変換が可能です。
シミュレーター上でiPhone8,iPhone11,iPadPro(9.7inch)を試しましたが、うまく行きました。
他の機種も実行出来ると思います。では、素敵なダークモードライフを!
- 投稿日:2020-04-21T17:54:28+09:00
カウントアプリを作ってみた話
実践形式で身に付くiOSアプリ開発の基本(型の変換,色の変更,if文,剰余)
この記事でわかる主なこと
1.Int型からString型への変換
2.テキストカラーの変更該当してそうなエラー
Cannot assign value of type 'Int' to type 'String'
最終的にできるもの
1.Int型からString型への変換
画面に表示されるUILabelはString型しか扱うできない。しかし、四則演算を行えるのはInt型やdouble型であるので、それらの型で計算したものをUILabelに表示するにはInt型からString型への変換を行わなければならない。
Int型からString型への変換を以下のように行っている。
let convert = String(countNumber)
これで安心してUIlabelに数値を入れることができる。
2.テキストカラーの変更
アプリの中では数字が3で割り切れる時に(変数)%3 == 0のようにすることで3で割り切れることを表現している。
今回は数字が3で割り切れる時のみ色を変更しているので、
numberLabel.textColor = UIColor.yellow
を条件の中に入れれば良い。
さらに、条件を満たさなかった時には色を戻す必要があるので、
numberLabel.textColor = UIColor.white
を条件を満たさなかった時のelse文の中に書く必要がある。
GitHubURL
- 投稿日:2020-04-21T17:54:28+09:00
実践形式で身に付くiOSアプリ開発の基本(型の変換,色の変更,if文,剰余)<カウントアプリ>
この記事でわかる主なこと
1.Int型からString型への変換
2.テキストカラーの変更該当してそうなエラー
Cannot assign value of type 'Int' to type 'String'
最終的にできるもの
1.Int型からString型への変換
画面に表示されるUILabelはString型しか扱うできない。しかし、四則演算を行えるのはInt型やdouble型であるので、それらの型で計算したものをUILabelに表示するにはInt型からString型への変換を行わなければならない。
Int型からString型への変換を以下のように行っている。
let convert = String(countNumber)
これで安心してUIlabelに数値を入れることができる。
2.テキストカラーの変更
アプリの中では数字が3で割り切れる時に(変数)%3 == 0のようにすることで3で割り切れることを表現している。
今回は数字が3で割り切れる時のみ色を変更しているので、
numberLabel.textColor = UIColor.yellow
を条件の中に入れれば良い。
さらに、条件を満たさなかった時には色を戻す必要があるので、
numberLabel.textColor = UIColor.white
を条件を満たさなかった時のelse文の中に書く必要がある。
GitHubURL
- 投稿日:2020-04-21T17:54:28+09:00
カウントアプリで型の変換,色の変更,if文,剰余について学ぶ
この記事でわかる主なこと
1.Int型からString型への変換
2.テキストカラーの変更解決できそうなエラー
Cannot assign value of type 'Int' to type 'String'最終的にできるもの
1.Int型からString型への変換
画面に表示されるUILabelはString型しか扱うできない。しかし、四則演算を行えるのはInt型やdouble型であるので、それらの型で計算したものをUILabelに表示するにはInt型からString型への変換を行わなければならない。
Int型からString型への変換を以下のように行っている。let convert = String(countNumber)これで安心してUIlabelに数値を入れることができる。
2.テキストカラーの変更
アプリの中では数字が3で割り切れる時に(変数)%3 == 0のようにすることで3で割り切れることを表現している。
今回は数字が3で割り切れる時のみ色を変更しているので、numberLabel.textColor = UIColor.yellowを条件の中に入れれば良い。
さらに、条件を満たさなかった時には色を戻す必要があるので、numberLabel.textColor = UIColor.whiteを条件を満たさなかった時のelse文の中に書く必要がある。
全体のソースコード
import UIKit class ViewController: UIViewController { var countNumber:Int = 0 @IBOutlet weak var numberLabel: UILabel! @IBAction func downButton(_ sender: Any) { countNumber += 1 let convert = String(countNumber) if countNumber % 3 == 0{ numberLabel.text = convert numberLabel.textColor = UIColor.yellow print("\(countNumber)") } else { numberLabel.text = convert numberLabel.text = convert numberLabel.textColor = UIColor.white } } @IBAction func upButton(_ sender: Any) { countNumber -= 1 let convert = String(countNumber) if countNumber % 3 == 0{ numberLabel.textColor = UIColor.yellow numberLabel.text = convert } else { numberLabel.text = convert numberLabel.textColor = UIColor.white } numberLabel.text = convert } override func viewDidLoad() { super.viewDidLoad() var convert = String(countNumber) numberLabel.text = convert numberLabel.textColor = UIColor.yellow } }GitHubURL
- 投稿日:2020-04-21T11:39:01+09:00
AppleWatchでログが出ないことがあるので、iPhoneに投げつけて無理やり表示
困ったこと
- AppleWatchアプリを開発している
- なんかAppleWatchのログがでない
- ログが出る人もいれば出ない人もいる現象??
もういいやiPhoneに投げつけよう
- WatchConnectivityのメッセージ送信でログ送信を作る
Watch側.swiftWCSession.default.sendMessage(["show_log": log], replyHandler: { _ in }) { _ in }iOS側.swiftfunc session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { guard let key = message.keys.first else { return } switch key { case "show_log": print(message[key]) case "その他いろいろ受け取るでしょうきっと"あわせてどうぞ
- 投稿日:2020-04-21T11:30:19+09:00
AppleWatchアプリをXcodeGenで構築した
AppleWatchアプリをXcodeGenで構築したので、project.ymlを大雑把に置いておきます
困ったこと
- AppleWatchでのXcodeGenの文献が少なかった
開発環境と構成
- Xcode11.X~
- 構成
- iOSAppTarget
- WatchKit App
- WatchKit Extension
- iOS13.X
- WatchOS6.X
- ツール系
- Mint: 0.13.0(Mintfile)
- XcodeGen: 2.15.1(project.yml)
- SwiftPM
- SwiftPMに対応しているライブラリがあれば優先的に
- SwiftLint: 0.39.2
- Carthage: 0.34.0(Cartfile)
- SwiftPMに対応してないが、Carthageに対応しているライブラリがあれば優先的に
- Bundler: 2.1.3(Gemfile)
- Cocoapods: 1.9.1(Podfile)
- SwiftPMにもCarthageにも対応してないライブラリ(Firebaseとか)
- Fastlane: 2.144.0(Fastfile)
- いろいろ
XcodeGenはMintで管理
- Mintを使って XcodeGen / SwiftLint / Carthage のバージョンを管理しました
- ライブラリはなるべくSwiftPMに対応していればそれを優先的に利用し、Carthage->CocoaPodsの順で優先度を割り振りました
XcodeGenのproject.yml
project.ymlattributes: LastSwiftUpdateCheck: 1130 LastUpgradeCheck: 1130 ORGANIZATIONNAME: inc.noplan settings: base: CURRENT_PROJECT_VERSION: 1 MARKETING_VERSION: "0.0.26" configs: Debug: debug Release: release name: [アプリ名] packages: Nuke: url: https://github.com/kean/Nuke.git from: 8.4.1 [SwiftPMで使用するライブラリたち] options: groupSortPosition: bottom transitivelyLinkDependencies: false settingGroups: Debug: ALWAYS_SEARCH_USER_PATHS: NO CLANG_ANALYZER_NONNULL: YES CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION: YES_AGGRESSIVE CLANG_CXX_LANGUAGE_STANDARD: gnu++14 CLANG_CXX_LIBRARY: libc++ CLANG_ENABLE_MODULES: YES CLANG_ENABLE_OBJC_ARC: YES CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING: YES CLANG_WARN_BOOL_CONVERSION: YES CLANG_WARN_COMMA: YES CLANG_WARN_CONSTANT_CONVERSION: YES CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS: YES CLANG_WARN_DIRECT_OBJC_ISA_USAGE: YES_ERROR CLANG_WARN_DOCUMENTATION_COMMENTS: YES CLANG_WARN_EMPTY_BODY: YES CLANG_WARN_ENUM_CONVERSION: YES CLANG_WARN_INFINITE_RECURSION: YES CLANG_WARN_INT_CONVERSION: YES CLANG_WARN_NON_LITERAL_NULL_CONVERSION: YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF: YES CLANG_WARN_OBJC_LITERAL_CONVERSION: YES CLANG_WARN_OBJC_ROOT_CLASS: YES_ERROR CLANG_WARN_RANGE_LOOP_ANALYSIS: YES CLANG_WARN_STRICT_PROTOTYPES: YES CLANG_WARN_SUSPICIOUS_MOVE: YES CLANG_WARN_UNGUARDED_AVAILABILITY: YES_AGGRESSIVE CLANG_WARN_UNREACHABLE_CODE: YES CLANG_WARN__DUPLICATE_METHOD_MATCH: YES COPY_PHASE_STRIP: NO DEBUG_INFORMATION_FORMAT: dwarf ENABLE_STRICT_OBJC_MSGSEND: YES ENABLE_TESTABILITY: YES GCC_C_LANGUAGE_STANDARD: gnu11 GCC_DYNAMIC_NO_PIC: NO GCC_NO_COMMON_BLOCKS: YES GCC_OPTIMIZATION_LEVEL: 0 GCC_PREPROCESSOR_DEFINITIONS: - $(inherited) - DEBUG=1 GCC_WARN_64_TO_32_BIT_CONVERSION: YES GCC_WARN_ABOUT_RETURN_TYPE: YES_ERROR GCC_WARN_UNDECLARED_SELECTOR: YES GCC_WARN_UNINITIALIZED_AUTOS: YES_AGGRESSIVE GCC_WARN_UNUSED_FUNCTION: YES GCC_WARN_UNUSED_VARIABLE: YES MTL_ENABLE_DEBUG_INFO: YES ONLY_ACTIVE_ARCH: YES PRODUCT_NAME: $(TARGET_NAME) SDKROOT: iphoneos SWIFT_ACTIVE_COMPILATION_CONDITIONS: DEBUG SWIFT_OPTIMIZATION_LEVEL: -Onone SWIFT_VERSION: 5.0 Staging: [割愛] Release: [割愛] schemes: \[scheme名]: build: targets: [iOSのターゲット名]: all run: config: Debug test: config: Debug profile: config: Debug analyze: config: Debug archive: config: Release \[いろいろiOSのscheme]: \[WatchOSのscheme名 WatchKit App]: build: targets: [WatchOSのターゲット名 WatchKit App]: all run: config: Debug test: config: Debug profile: config: Debug analyze: config: Debug archive: config: Release \[WatchOSのscheme名 WatchKit Extension]: build: targets: [WatchOSのターゲット名 WatchKit Extension]: all run: config: Debug test: config: Debug profile: config: Debug analyze: config: Debug archive: config: Release targets: \[iOSのターゲット名]: dependencies: - target: [WatchOSのターゲット WatchKit App] - {embed: false, framework: Pods_[iOSのターゲット].framework} - {embed: false, framework: StoreKit.framework} - package: SDWebImageSwiftUI - package: SwiftUIX - carthage: SwiftyStoreKit - carthage: SwiftDate - carthage: ObjectMapper platform: iOS postbuildScripts: - inputFiles: - ${PODS_PODFILE_DIR_PATH}/Podfile.lock - ${PODS_ROOT}/Manifest.lock name: '[CP] Check Pods Manifest.lock' outputFiles: - $(DERIVED_FILE_DIR)/Pods-[プロジェクト]-checkManifestLockResult.txt runOnlyWhenInstalling: false script: "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n" shell: /bin/sh showEnvVars: false - inputFileLists: - ${PODS_ROOT}/Target Support Files/Pods-{iOSのターゲット}/Pods-{iOSのターゲット}-frameworks-${CONFIGURATION}-input-files.xcfilelist name: '[CP] Embed Pods Frameworks' outputFileLists: - ${PODS_ROOT}/Target Support Files/Pods-{iOSのターゲット}/Pods-{iOSのターゲット}-frameworks-${CONFIGURATION}-output-files.xcfilelist runOnlyWhenInstalling: false script: '"${PODS_ROOT}/Target Support Files/Pods-{iOSのターゲット}/Pods-{iOSのターゲット}-frameworks.sh"' shell: /bin/sh showEnvVars: false prebuildScripts: - name: run swiftlint script: | if mint which swiftlint >/dev/null; then mint run swiftlint autocorrect --format mint run swiftlint else echo "warning: mint install swiftlint" fi - name: linence plist script: | ${PODS_ROOT}/LicensePlist/license-plist --output-path $PRODUCT_NAME/Settings.bundle --config-path $PRODUCT_NAME/license_plist.yml /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:2:DefaultValue ${MARKETING_VERSION}" "$PRODUCT_NAME/Settings.bundle/Root.plist" /usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:3:DefaultValue ${CURRENT_PROJECT_VERSION}" "$PRODUCT_NAME/Settings.bundle/Root.plist" settings: configs: Debug: ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon CLANG_ENABLE_MODULES: YES CODE_SIGN_ENTITLEMENTS: [ENTITLEMENTSのpath].entitlements CODE_SIGN_IDENTITY: iPhone Developer CODE_SIGN_STYLE: Automatic DEVELOPMENT_ASSET_PATHS: '"[DEVELOPMENT_ASSETのpath]/Preview Content"' DEVELOPMENT_TEAM: [TEAMをどうぞ] ENABLE_PREVIEWS: YES FRAMEWORK_SEARCH_PATHS: - $(inherited) - '"."' INFOPLIST_FILE: [plistのpath]/Info.plist LD_RUNPATH_SEARCH_PATHS: - $(inherited) - '@executable_path/Frameworks' PRODUCT_BUNDLE_IDENTIFIER: [PRODUCTのBUNDLE_IDENTIFIER] PRODUCT_NAME: $(TARGET_NAME) SDKROOT: iphoneos SWIFT_OBJC_BRIDGING_HEADER: [Bridging-Headerのpath/Bridging-Header.h] SWIFT_OPTIMIZATION_LEVEL: -Onone SWIFT_VERSION: 5.0 TARGETED_DEVICE_FAMILY: 1,2 Staging: [割愛] Release: [割愛] sources: - [デフォルトだとtarget名がフォルダ名と同じになってる] - name: GoogleService-Info.plist path: GoogleService-Info.plist group: [ファイルはrootにあるけどまとめたいのでフォルダを指定する] type: application deploymentTarget: "13.0" attributes: SystemCapabilities: com.apple.Push: enabled: 1 com.apple.InAppPurchase: enabled: 1 \[WatchOSのプロジェクト WatchKit App]: dependencies: - target: [WatchOSのtarget名 WatchKit Extension] - {embed: false, framework: Pods_[プロジェクト]_WatchKit_App.framework} platform: watchOS postbuildScripts: - inputFiles: - ${PODS_PODFILE_DIR_PATH}/Podfile.lock - ${PODS_ROOT}/Manifest.lock name: '[CP] Check Pods Manifest.lock' outputFiles: - $(DERIVED_FILE_DIR)/Pods-[プロジェクト] WatchKit App-checkManifestLockResult.txt runOnlyWhenInstalling: false script: "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n" shell: /bin/sh showEnvVars: false settings: configs: Debug: ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: YES ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: [チーム] FRAMEWORK_SEARCH_PATHS: - $(inherited) - '"."' IBSC_MODULE: [IBSC_MODULE]_WatchKit_Extension INFOPLIST_FILE: [プロジェクト] WatchKit App/Info.plist LD_RUNPATH_SEARCH_PATHS: - $(inherited) - '@executable_path/Frameworks' PRODUCT_BUNDLE_IDENTIFIER: [アプリのBUNDLE_IDENTIFIER].watchkitapp PRODUCT_NAME: $(TARGET_NAME) SDKROOT: watchos SKIP_INSTALL: YES SWIFT_VERSION: 5.0 TARGETED_DEVICE_FAMILY: 4 WATCHOS_DEPLOYMENT_TARGET: 6.1 Stating: [割愛] Release: [割愛] sources: - name: [デフォルトだとtarget名がフォルダ名と同じになってるやつ] WatchKit App type: application.watchapp2 deploymentTarget: "6.0" \[WatchOSのプロジェクト] WatchKit Extension: dependencies: - {embed: false, framework: Pods_[プロジェクト]_WatchKit_Extension.framework} - carthage: ObjectMapper - package: Nuke platform: watchOS postbuildScripts: - inputFiles: - ${PODS_PODFILE_DIR_PATH}/Podfile.lock - ${PODS_ROOT}/Manifest.lock name: '[CP] Check Pods Manifest.lock' outputFiles: - $(DERIVED_FILE_DIR)/Pods-[プロジェクト] WatchKit Extension-checkManifestLockResult.txt runOnlyWhenInstalling: false script: "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n" shell: /bin/sh showEnvVars: false settings: configs: Debug: ASSETCATALOG_COMPILER_COMPLICATION_NAME: Complication CODE_SIGN_STYLE: Automatic DEVELOPMENT_ASSET_PATHS: '"[プロジェクト] WatchKit Extension/Preview Content"' DEVELOPMENT_TEAM: 83GH78U9V5 ENABLE_PREVIEWS: YES FRAMEWORK_SEARCH_PATHS: - $(inherited) - '"."' INFOPLIST_FILE: [プロジェクト] WatchKit Extension/Info.plist LD_RUNPATH_SEARCH_PATHS: - $(inherited) - '@executable_path/Frameworks' - '@executable_path/../../Frameworks' PRODUCT_BUNDLE_IDENTIFIER: [アプリのBUNDLE_IDENTIFIER].watchkitapp.watchkitextension PRODUCT_NAME: ${TARGET_NAME} SDKROOT: watchos SKIP_INSTALL: YES SWIFT_VERSION: 5.0 TARGETED_DEVICE_FAMILY: 4 WATCHOS_DEPLOYMENT_TARGET: 6.1 Staging: [割愛] Release: [割愛] sources: - [デフォルトだとtarget名がフォルダ名と同じになってるやつ] WatchKit Extension - name: [iOSでもWatchOSでも使いたいファイル].swift path: [path/to/ファイルの所在地].swift group: [置いておきたいpath/to/DataModel] type: watchkit2-extension deploymentTarget: "6.0"あわせてどうぞ
- 投稿日:2020-04-21T10:25:31+09:00
AppleWatchアプリが更新されないかもしれない
- 投稿日:2020-04-21T08:05:44+09:00
iPadOS 13.4のポインタに対応する
2020年3月にiPadOS 13.4がリリースされ、ポインタが導入されました。iPadにマウスやトラックパッドを接続(有線もしくは無線)して利用できます。
対応端末
すべてのiPad Proモデル
iPad Air 2以降
iPad(第5世代)以降
iPad mini 4以降4~5年前のモデルでも対応しており、幅広い端末で利用できます。
Magnetic Effect
ボタンの上にポインタを移動させると、ボタンに飛びつくようなアニメーションとともに、ポインタがボタンの形に変化します。またポインタが離れる際には、ボタンにしがみつくようなアニメーションがあり、これらは「Magnetic Effect」と呼ばれています。
エフェクトは3種類
ポインタがボタンの上に移動した時のエフェクトは3種類用意されています。
Highlight:
ポインタがボタンの形に変わります。ツールバーのアイコンやメニューなどで使われています。
Lift:
ボタンが拡大され、ポインタは非表示になります。ホーム画面のアイコンで使われています。
Hover:
オーバーレイ(もしくはアンダーレイ)が表示されます。他にもオプションでボタンを拡大したり、シャドーをつけたり、ポインタの形を変えたりする事も可能です。カスタマイズすることで様々なエフェクトを実装できます。
ポインタの形は自由
ポインタの形は
UIBezierPath
で表現できる形であれば対応しています。例えばKeynoteではテキストをリサイズする時にポインタの形が変わります。
UIPointerInteraction API
ポインタ対応には
UIPointerInteraction API
を使います。ただし、UIKitでは
UIButton
、UIBarButtonItem
、UISegmentedControl
、UIMenuController
などがすでにポインタに対応しています。例えばUIButtonをポインタに対応させるためには、以下のコードを実行します。if #available(iOS 13.4, *) { button.isPointerInteractionEnabled = true }デフォルトでは
Highlight
エフェクトが適用されますが、別のエフェクトやポインタの形を適用させたい場合はpointerStyleProvider
を使います。if #available(iOS 13.4, *) { button.pointerStyleProvider = { button, effect, shape in if case let .roundedRect(frame, radius) = shape { // デフォルトよりもハイライトエリアを3ptsずつ広げる let rect = CGRect(x:frame.origin.x-3, y:frame.origin.y-3, width:frame.width+6, height:frame.height+6) return UIPointerStyle(effect: effect, shape: .roundedRect(rect, radius: radius)) } return nil } }その他のビューでポインタに対応するには、
UIPointerInteraction
を登録します。使い方はドラッグ&ドロップを実装するためのUIDragInteraction
/UIDropInteraction
や、コンテキストメニューを実装するためのUIContextMenuInteraction
と同様です。class TitleView: UIView { private func setupUI() { ... if #available(iOS 13.4, *) { enablePointer() } } .... } @available(iOS 13.4, *) extension TitleView: UIPointerInteractionDelegate { func enablePointer() { isUserInteractionEnabled = true addInteraction(UIPointerInteraction(delegate: self)) } func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? { // ポインタが反応すべきframeを返す return UIPointerRegion(rect: rectForTitle()) } private func rectForTitle() -> CGRect { // AttributedTextが表示されているrectを返す return ... } func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { // ポインタのエフェクト(UIPointerEffect)や形(UIPointerShape)を返す let targetedPreview = UITargetedPreview(view: self) let effect: UIPointerEffect = .highlight(targetedPreview) let shape: UIPointerShape = .roundedRect(rectForTitle(), radius: UIPointerShape.defaultCornerRadius) let pointerStyle = UIPointerStyle(effect: effect, shape: shape) return pointerStyle } }実際の動きは以下のようになります。
UIView
でもちゃんとポインタが反応するようになりました。
上記の例では
Highlight
エフェクトを利用していますが、例えばHover
エフェクトでオーバーレイを表示させたい場合はpointerInteraction(_:styleFor:)
を次のように実装します。@available(iOS 13.4, *) extension TitleView: UIPointerInteractionDelegate { func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { let targetedPreview = UITargetedPreview(view: self) targetedPreview.parameters.visiblePath = UIBezierPath(roundedRect: rectForTitle(), cornerRadius: 10.0) // Hoverエフェクトを利用する let effect: UIPointerEffect = .hover(targetedPreview, preferredTintMode: .overlay, prefersShadow: false, prefersScaledContent: false) let pointerStyle = UIPointerStyle(effect: effect) return pointerStyle } }動きはこのようになります。
Highlight
と違って、Hover
ではポインタの形は変わらず、オーバーレイのみが表示されます。Magnetic Effectも適用されません。
もしHighlight
,Lift
,Hover
以外のまったく違うエフェクトを適用したい場合は、UIPointerInteractionDelegate
のpointerInteraction(_:willEnter:animator:)
/pointerInteraction(_:willExit:animator:)
を使って独自のアニメーションを実装できます。@available(iOS 13.4, *) extension TitleView: UIPointerInteractionDelegate { func pointerInteraction(_ interaction: UIPointerInteraction, willEnter region: UIPointerRegion, animator: UIPointerInteractionAnimating) { // ポインタがビューの上に移動した時に呼ばれる showEventResizingHandler(true) } func pointerInteraction(_ interaction: UIPointerInteraction, willExit region: UIPointerRegion, animator: UIPointerInteractionAnimating) { // ポインタがビューの外に移動した時に呼ばれる animator.addAnimations { self.showEventResizingHandler(false) } } }拙作カレンダーアプリでは予定にポインタを移動させるとハンドラーが表示され、それをドラッグする事で予定の長さを変更する、という機能の実装に利用しています。
最後に、指とポインタによる入力を区別したい場合があります。例えば上記の「予定の端をドラッグして、予定の長さを変える」という機能ですが、ポインタを利用している時には便利な機能ですが、指を使って予定の端をドラッグするというのは至難の技です。また、そもそも指で操作している時にはリサイズのためのハンドラーが表示されないという問題もあります。このような場合、
Info.plist
にUIApplicationSupportsIndirectInputEvents
キーを追加することで、UITouch.type
の値が指の場合はUITouch.TouchType.direct
、ポインタの場合はUITouch.TouchType.indirect
となり、指とポインタを区別する事ができます。class CalendarView: UIView { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { if #available(iOS 13.4, *), touches.first?.type == .some(.indirectPointer) { // ポインタによる操作 ... } else { // 指による操作 ... } } }ただしこのキーを追加すると、例えばカスタムの
UIGestureRecognizer
を実装している場合、UIGestureRecognizer.numberOfTouches
が0になるケースがあり、その結果UIGestureRecognizer.location(ofTouch:in:)
でクラッシュが発生するなどの副作用するため、アプリを再度テストする必要があります。詳しくはこちらのドキュメントをご覧ください。参考
Pointer Interactions
https://developer.apple.com/documentation/uikit/pointer_interactionsHuman Interface Guideline: Pointers (iPadOS)
https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/pointers/UIApplicationSupportsIndirectInputEvents
https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputeventsSupporting Pointer Interactions
https://pspdfkit.com/blog/2020/supporting-pointer-interactions/