程序调试 (六) —— 使用Build Configurations 和 .xcconfig 构建你的App(二)

版本记录

版本号 时间
V1.0 2021.06.09 星期三

前言

程序总会有bug,如果有好的调试技巧和方法,那么就是事半功倍,这个专题专门和大家分享下和调试相关的技巧。希望可以帮助到大家。感兴趣的可以看下面几篇文章。
1. 程序调试 (一) —— App Crash的调试和解决示例(一)
2. 程序调试 (二) —— Xcode Simulator的高级功能(一)
3. 程序调试 (三) —— Xcode Simulator的高级功能(二)
4. 程序调试 (四) —— Xcode内存管理(一)
5. 程序调试 (五) —— 使用Build Configurations 和 .xcconfig 构建你的App(一)

源码

1. Swift

首先看下工程组织结构:

下面看下源码。

1. Base.xcconfig
// https://help.apple.com/xcode/#/dev745c5c974

APP_NAME = Ninja Counter

BASE_BUNDLE_IDENTIFIER = com.raywenderlich.NinjaCounter

PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER)

USER_DEFAULTS_SUITE_NAME = group.$(BASE_BUNDLE_IDENTIFIER)

ONLY_ACTIVE_ARCH[config=Debug][sdk=*][arch=*] = YES
2. Debug.xcconfig
#include "Base.xcconfig"

APP_NAME = $(inherited)Dev

ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-NonProd

USER_DEFAULTS_RECORDS_KEY = HatchlingsRecords-Debug
3. Staging.xcconfig
#include "Base.xcconfig"

APP_NAME = $(inherited)QA

ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-NonProd

USER_DEFAULTS_RECORDS_KEY = HatchlingsRecords-Staging`
4. Release.xcconfig
#include "Base.xcconfig"

APP_NAME = $(inherited)

ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon

USER_DEFAULTS_RECORDS_KEY = HatchlingsRecords
5. WidgetBase.xcconfig
#include "Base.xcconfig"

PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).widget
6. WidgetDebug.xcconfig
#include "WidgetBase.xcconfig"
#include "Debug.xcconfig"
7. WidgetStaging.xcconfig
#include "WidgetBase.xcconfig"
#include "Staging.xcconfig"
8. WidgetRelease.xcconfig
#include "WidgetBase.xcconfig"
#include "Release.xcconfig"
9. NinjaCounterApp.swift
import SwiftUI

@main
struct NinjaCounterApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView(hatchlings: Hatchling.generatePreviewHatchlings())
    }
  }
}
10. ContentView.swift
import SwiftUI

struct ContentView: View {
  @State var hatchlings: [Hatchling] = []
  @State private var tag: String = ""

  var body: some View {
    NavigationView {
      VStack {
        List(hatchlings, id: \.id) { ninja in
          HStack {
            Text(ninja.tag)

            Spacer()

            Text("Hatch time")
              .font(.footnote)
              .foregroundColor(.green)

            Text(formatDate(ninja.date))
          }
        }
        .listStyle(InsetGroupedListStyle())
        .navigationBarTitle("Ninja Counter")
        .navigationBarItems(trailing:
          Button("Clear") {
            hatchlings = []
            UserDefaultsHelper.clearRecords()
          })

        VStack {
          Divider()
          HStack {
            Text("Tag:")
              .padding(.leading)
              .foregroundColor(Color("rw-dark"))
            TextField("Leonardo", text: $tag)
              .textFieldStyle(RoundedBorderTextFieldStyle())
              .padding(.trailing)
          }
          .padding(.top)

          Button("+ Hatchling") {
            recordHatchling()
          }
          .padding(.bottom)
          .font(.largeTitle)
        }
      }
    }
    .onAppear(perform: loadProducts)
    .accentColor(Color("rw-green"))
  }


  func loadProducts() {
    hatchlings = UserDefaultsHelper.getRecords()
  }

  func recordHatchling() {
    var hatchling = Hatchling(tag: tag, date: Date())
    if hatchling.tag.isEmpty {
      let newTag = String(hatchling.id.uuidString.suffix(6))
      hatchling.tag = newTag
    }
    hatchlings.append(hatchling)
    UserDefaultsHelper.persistRecords(hatchlings)
  }

  func formatDate(_ date: Date) -> String {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "MM/dd HH:mm:SS"
    return dateFormatter.string(from: date)
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView(hatchlings: Hatchling.generatePreviewHatchlings())
  }
}
11. Hatchling.swift
import Foundation

struct Hatchling: Codable {
  var id = UUID()
  var tag: String
  var date: Date

  static func generatePreviewHatchlings() -> [Hatchling] {
    let leo = Hatchling(tag: "Leonardo", date: Date())
    let don = Hatchling(tag: "Donatello", date: Date())

    return [leo, don]
  }
}
12. UserDefaultsHelper.swift
import Foundation

enum UserDefaultsHelper {
  static private let defaults =
    UserDefaults(suiteName: Config.stringValue(forKey: "USER_DEFAULTS_SUITE_NAME")) ?? .standard

  static private let recordsKey = Config.stringValue(forKey: "USER_DEFAULTS_RECORDS_KEY")

  static func getRecords() -> [Hatchling] {
    guard
      let objects = defaults.value(forKey: recordsKey) as? Data,
      let hatchlings = try? JSONDecoder().decode([Hatchling].self, from: objects)
    else {
      return []
    }

    return hatchlings
  }

  static func persistRecords(_ array: [Hatchling]) {
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(array) {
      defaults.set(encoded, forKey: recordsKey)
    }
  }

  static func clearRecords() {
    defaults.removeObject(forKey: recordsKey)
  }

  static func getRecordsCount() -> Int {
    return getRecords().count
  }
}
13. Config.swift
import Foundation

enum Config {
  static func stringValue(forKey key: String) -> String {
    guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String else {
      fatalError("Invalid value or undefined key")
    }

    return value
  }
}
14. Widget.swift
import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
  func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), count: 0, latest: "--")
  }

  func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
    let entry = SimpleEntry(
      date: Date(),
      count: UserDefaultsHelper.getRecordsCount(),
      latest: UserDefaultsHelper.getRecords().last?.tag ?? "--")
    completion(entry)
  }

  func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
    var entries: [SimpleEntry] = []

    //    Generate a timeline consisting of five entries an hour apart, starting from the current date.
    let currentDate = Date()
    for hourOffset in 0 ..< 5 {
      if let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate) {
        let entry = SimpleEntry(
          date: entryDate,
          count: UserDefaultsHelper.getRecordsCount(),
          latest: UserDefaultsHelper.getRecords().last?.tag ?? "--")
        entries.append(entry)
      }
    }

    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
  }
}

struct SimpleEntry: TimelineEntry {
  var date: Date
  let count: Int
  let latest: String
}

struct WidgetEntryView: View {
  var entry: Provider.Entry

  var body: some View {
    VStack {
      Text("Hatchlings")
        .bold()

      Text("\(entry.count)")

      Divider()

      Text("Latest Recorded")
        .bold()

      Text("\(entry.latest)")
    }
  }
}

@main
struct CounterWidget: Widget {
  let kind: String = "widget"

  var body: some WidgetConfiguration {
    StaticConfiguration(kind: kind, provider: Provider()) { entry in
      WidgetEntryView(entry: entry)
    }
    .configurationDisplayName("My Widget")
    .description("This is an example widget.")
  }
}

struct Widget_Previews: PreviewProvider {
  static var previews: some View {
    WidgetEntryView(entry: SimpleEntry(date: Date(), count: 4, latest: "Raphael"))
      .previewContext(WidgetPreviewContext(family: .systemSmall))
  }
}

后记

本篇主要讲述了使用Build Configurations.xcconfig 构建你的App,感兴趣的给个赞或者关注~~~

推荐阅读更多精彩内容