20200421のSwiftに関する記事は9件です。

Swiftでapiを使って天気予報を取得しよう

はじめに

Swift初心者が学習を兼ねて、作ってみました。

環境

Swift 5.1.2
Xcode 11.2

使用したお天気api
http://weather.livedoor.com/weather_hacks/webservice

Podをインストールする

Podfile
  pod "Alamofire" 
  pod "SwiftyJSON"

この2つのライブラリをインストールする事で、
簡単にHTTP通信と、JSONを扱うことができます。

Main.storyboardを作る

main.pngresult.png

こんな感じでパーツを配置します。
今回はpickerViewを使用して、地域を選択できるようにします。
あとはラベルとボタンを、いい感じに配置します。

ボタンを押すと、選択した地域の天気予報を取得し、次の画面でラベルに反映するようにします。
とてもシンプルです。

また、今回はNavigation Controllerを使用しています。
タブのEditor -> Embed In -> Navigation Controllerで使用できます。

実装する

まずは、pickerViewに入れるデータをmodelに書いておきます。

CityModel.swift
import Foundation

class CityModel {

    //地域の名前とURL追加するプロパティを持ちます。   
    let code: String
    let name: String

    init(cityCode:String, cityName:String) {
        code = cityCode
        name = cityName
    }
}
CityList.swift
import 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.swift
import 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.swift
import 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
        }
    }
}

完成

main2.pngresult2.png

コードの内容はさておき、天気予報のデータの取得に成功しました。

ViewDidLoad内でフォントサイズボタンの枠線を変更していますが、
長くなるので割愛しました。お好みでどうぞ。

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

Xcodeのシミュレーターでダークモードに切り替えるキーボードショートカット

初めに

Xcodeのシミュレーターでダークモードを使える機能が実装されていましたが、
意外にもショートカットを使った記事が無かったので備忘録がわりに掲載します。

環境

Xcode11.4
(注:Xcode11.3では実行できなかった)


参考記事
iOS シミュレーターでダークモードを切り替える2つの方法(画像付き)
https://qiita.com/zono_Bianchi/items/e79ddc6b6bc52d854fcf

操作方法

Command(⌘) + Shift(⇧) + A

これだけで簡単にシミュレーターでダークモードへの変換が可能です。

シミュレーター上でiPhone8,iPhone11,iPadPro(9.7inch)を試しましたが、うまく行きました。
他の機種も実行出来ると思います。

QiitaLightDarkModeChange 3.gif

では、素敵なダークモードライフを!

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

カウントアプリを作ってみた話

実践形式で身に付くiOSアプリ開発の基本(型の変換,色の変更,if文,剰余)

この記事でわかる主なこと

1.Int型からString型への変換
2.テキストカラーの変更

該当してそうなエラー

Cannot assign value of type 'Int' to type 'String'

最終的にできるもの

Simulator Screen Shot - iPhone 11 - 2020-04-21 at 17.27.37.png
Simulator Screen Shot - iPhone 11 - 2020-04-21 at 17.27.30.png

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

https://github.com/Hyperbolic4183/CountApp

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

実践形式で身に付く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

https://github.com/Hyperbolic4183/CountApp

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

カウントアプリで型の変換,色の変更,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

https://github.com/Hyperbolic4183/CountApp

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

AppleWatchでログが出ないことがあるので、iPhoneに投げつけて無理やり表示

困ったこと

  • AppleWatchアプリを開発している
  • なんかAppleWatchのログがでない
  • ログが出る人もいれば出ない人もいる現象??

もういいやiPhoneに投げつけよう

  • WatchConnectivityのメッセージ送信でログ送信を作る
Watch側.swift
WCSession.default.sendMessage(["show_log": log], replyHandler: { _ in }) { _ in }
iOS側.swift
func 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 "その他いろいろ受け取るでしょうきっと"

あわせてどうぞ

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

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.yml
attributes:
  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"


あわせてどうぞ

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

AppleWatchアプリが更新されないかもしれない

困ったこと

  • XcodeでWatchOSアプリを開発していて、Runし直すけどなぜかWatchにコードが反映されないときがある
  • iPhoneアプリは更新されてるっぽいが、AppleWatchのアプリはアプリのインストールぐるぐるにならない

解決策

  • WatchKit AppのInterface.storyboardなどどこかUIを一部変更すると、AppleWatchアプリが更新される

再現環境

  • Xcode11.X~
  • 構成
    • iOSAppTarget
    • WatchKit App
    • WatchKit Extension
  • iOS13.X
  • WatchOS6.X

あわせてどうぞ

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

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」と呼ばれています。
78849069-70661f00-7a4e-11ea-8e03-ada6cdf77bea.gif

エフェクトは3種類

ポインタがボタンの上に移動した時のエフェクトは3種類用意されています。

Highlight: ポインタがボタンの形に変わります。ツールバーのアイコンやメニューなどで使われています。
78849152-9d1a3680-7a4e-11ea-9150-a4bc9296e97e.gif

Lift: ボタンが拡大され、ポインタは非表示になります。ホーム画面のアイコンで使われています。
78849165-a3a8ae00-7a4e-11ea-839d-661a3a44ab22.gif

Hover: オーバーレイ(もしくはアンダーレイ)が表示されます。他にもオプションでボタンを拡大したり、シャドーをつけたり、ポインタの形を変えたりする事も可能です。カスタマイズすることで様々なエフェクトを実装できます。
78849173-aacfbc00-7a4e-11ea-93ad-8f3cae3cc417.gif

ポインタの形は自由

ポインタの形はUIBezierPathで表現できる形であれば対応しています。例えばKeynoteではテキストをリサイズする時にポインタの形が変わります。
78849227-d2bf1f80-7a4e-11ea-989b-e476c3a26791.gif

UIPointerInteraction API

ポインタ対応にはUIPointerInteraction APIを使います。

ただし、UIKitではUIButtonUIBarButtonItemUISegmentedControlUIMenuControllerなどがすでにポインタに対応しています。例えば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でもちゃんとポインタが反応するようになりました。
ezgif-6-a28da0dbee9e.gif

上記の例では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も適用されません。
ezgif-6-a9d7c3f33eae.gif
もしHighlight, Lift, Hover以外のまったく違うエフェクトを適用したい場合は、UIPointerInteractionDelegatepointerInteraction(_: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)
        }
    }
}

拙作カレンダーアプリでは予定にポインタを移動させるとハンドラーが表示され、それをドラッグする事で予定の長さを変更する、という機能の実装に利用しています。
ezgif-6-49bffc550953.gif
最後に、指とポインタによる入力を区別したい場合があります。例えば上記の「予定の端をドラッグして、予定の長さを変える」という機能ですが、ポインタを利用している時には便利な機能ですが、指を使って予定の端をドラッグするというのは至難の技です。また、そもそも指で操作している時にはリサイズのためのハンドラーが表示されないという問題もあります。

このような場合、Info.plistUIApplicationSupportsIndirectInputEventsキーを追加することで、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:)でクラッシュが発生するなどの副作用するため、アプリを再度テストする必要があります。詳しくはこちらのドキュメントをご覧ください。

参考

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