Swift(UI)でのiPhoneアプリ開発時のTips

最近、数ヶ月で数本のiPhoneアプリを開発して公開しました。その際に、個人的な Tips がそこそこ溜まってきたので備忘録として記載しときます。一つ一つは小さなTipsですが毎回いざというときに検索して毎回調べてしまいます…

デバイスの各種サイズを取得する

# デバイス全体サイズ
UIScreen.main.bounds.size

# 上部のセーフエリアのサイズ
UIApplication.shared.windows[0].safeAreaInsets

Textのフォントを指定する

Text("ほげほげ")
  .font(.custom("Avenir-Roman", size: 20))

戻るや閉じるの動作をプログラムで

struct ExamplelView: View {
  @Environment(\.presentationMode) var presentation

  var body: some View {
    ...
    self.presentation.wrappedValue.dismiss()
    ...
  }
} 

起動時の初期化処理を描く場所

@main
struct ExampleApp: App {
  @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
  var body: some Scene {
    WindowGroup {
      Text("Hello World")
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    
    // ここに初期化等処理を書く

    return true
  }
}

バックグラウンドからの起動、URLスキームからの起動の捕捉

@main
struct ExampleApp: App {
  @Environment(\.scenePhase) private var scenePhase
  var body: some Scene {
    WindowGroup {
      SomeMainView()
        .onOpenURL(perform: { url in
          // URLスキームからの起動の場合、ここが呼び出される
        })
    }.onChange(of: scenePhase) { phase in
      if phase == .active {
        FireStorageManager.sentUserStats()
      }
   }
}

Viewオブジェクトを角丸の枠で囲む

SomeView()
  .overlay(
    RoundedRectangle(cornerRadius: 5)
    .stroke(Color.gray, lineWidth: 2)
  )

NavigationViewのヘッダを消す

NavigationView {
  VStack {
  }.navigationBarTitle("hoge")
  .navigationBarHidden(true)
}

navigationBarHidden を指定すればOKだが navigationBarTitle も指定しないと効かない…

ListのUIがなんか嫌(余白がある)

最近のバージョンからデフォルトがそうなったらしい

List {
  ...
}.listStyle(PlainListStyle())

通信元コンテンツを変更したがHTTPS取得しても変わらない(Alamofire)

OS側でよしなにキャッシュしている場合があるらしい。以下のコードを通信前に呼び出せばキャッシュが消える(最新を取得するようになる)

URLCache.shared.removeAllCachedResponses()

JSON を Codable オブジェクトに変換する

通信で取得したJSON文字列を Codable なオブジェクトに変換する方法

struct ExampleObject: Codable {
  let persons: [Person]
}

struct Person: Codable {
  let name: String
  let age: Int
  let addressString: String
  
  // JSONのタグ名と異なキーとしたい場合は CodingKeys でマッピングさせる
  enum CodingKeys: String, CodingKey {
    case name
    case age
    case addressString = "address_string"
  }
}

こういうオブジェクトを作って例えば、Alamofire のレスポンスを例にすると responseEntity が ExampleObject 型になる。

guard let data = response.data else {
  return
}
guard let responseEntity = try? JSONDecoder().decode(ExampleObject.self, from: data) else {
  return
}

Codable オブジェクトを作りたいが変換元のJSONがルート直下がArrayな場合

上の例 ExampleObject がそういうケースだった場合の例。以下のようなオマジナイを追加する。

struct ExampleObject: Codable {
  let persons: [Person]    // "persons" Key は実際にはJSONには存在しない
}

extension ExampleObject {
    init(from decoder: Decoder) throws {
        var persons: [Person] = []
        var unkeyedContainer = try decoder.unkeyedContainer()
        while !unkeyedContainer.isAtEnd {
            let person = try unkeyedContainer.decode(Person.self)
            persons.append(person)
        }
        self.init(persons: persons)
    }
}

extension ExampleObject {
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        for person in persons {
            try container.encode(person)
        }
    }
}

UIView を SwiftUIで利用したい

たとえば、GoogleMap (GMSMapView) の場合を例に… 他のViewも同じ。 ViewControllerの場合は UIViewRepresentable を UIViewControllerRepresentable に変更して微修正。

import SwiftUI
import GoogleMaps

struct MapView: UIViewRepresentable {
    typealias UIViewType = GMSMapView
    let mapView = GMSMapView(frame: .zero)
    
    func makeCoordinator() -> Coordinator {
        Coordinator(mapView: mapView)
    }
 
    func makeUIView(context: Context) -> GMSMapView {
        // 初期化処理などはココに書く        
        return mapView
    }
    
    func updateUIView(_ uiView: GMSMapView, context: Context) {
        // 描画等の処理はココに書く
        // 呼び出されるタイミングは SwiftUI のStateオブジェクト変化時など        
    }
    
    final class Coordinator: NSObject, GMSMapViewDelegate {
        init(mapView: GMSMapView) {
            super.init()
            mapView.delegate = self
        }
        
        // Delegate の処理はココに追加する
        func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
            return false
        }
        func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
        }
    }    
}

AppStoreに公開したがアプリ紹介のアプリ言語が「英語(EN)」となる

Appleに問い合わせたら

App Store 上で表示される言語情報は、App バンドルのローカリゼーションフォルダ (.lproj) によって決められます。App バンドル内でローカリゼーションが未設定もしくは無効になってしまう問題は、通常 Xcode のプロジェクトでのローカリゼーションの誤設定が原因です。

と返信をもらった。よくわからんかったが、以下のようにしたら解決した。

  1. XCodeにてプロジェクト設定 Info -> Localization で Japanese を追加する — ①
  2. 新規ファイルで String File タイプで作成し名称をInfoPlist.strings とする
  3. InfoPlist.string のプロパティで Japanese にチェックを入れる — ②

おそらくこれで問題が解決される。 InfoPlist.string が空ファイルなのが気持ち悪いのでよくある、Info.plistの設定値のローカライズ文言も私は加えておきます。

"NSLocationWhenInUseUsageDescription" = "現在地の利用を許可頂くと地図モードで現在地が素早く確認できたり、ルートの検索ができるようになります";
"NSUserTrackingUsageDescription" = "「許可」するとお客様に最適化された広告が表示されます";

AppStoreに公開されたがAdMobでリンクできない(検索で出てこない)

問題

AdMobでアプリに対してAppStoreのアプリ設定(リンク)をする必要がある。マニュアルでは「AppStoreに公開後、数日から数週間でAdMobの設定画面から検索できるようになります。」とあるが、いくら待っても検索でリンクさせたいアプリがでてこない

google-admob-ads-sdk という Google Group にてリクエストのメッセージを投稿するとすぐに対応してくれる。私は以下のように書いた。

投稿内容
Subject: Can't find my app to link to admob

I have released three my apps on app store month ago, but I can't find that on admob for linking.
Here my app store links


https://apps.apple.com/jp/app/michinori/id1560954019
https://apps.apple.com/jp/app/tokyo%E9%A7%85%E6%8E%A2%E7%B4%A2/id1563783099
https://apps.apple.com/jp/app/the-%E5%AE%9D%E5%A1%9A/id1566879484

Thank you so much for helping

数時間後返信もらって瞬殺で問題が解決された。

M1 Mac で Cocoapod周りでエラーがでる

時代が追いついていないらしい。対処方法は

  • XCodeのアプリの設定 TARGET -> Build Settings の Excluded Architectures の Debug / Release に 「Any iOS Simulator SDK = arm64」を登録する — ①
  • Podfile に以下を追記する
target 'SampleApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for SampleApp
  ...

  post_install do |installer|
    installer.pods_project.build_configurations.each do |config|
      config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
    end
  end
end
  • pod install コマンドは Rosetta を経由して起動したターミナルで行う — ②

以上です。まだまだ書ききれない色々な項目がありそうですので追って別記事で追記します。