HackingWithSwift-1
https://www.youtube.com/playlist?list=PLuoeXyslFTuZRi4q4VT6lZKxYbr7so1Mr
WeSplitApp
核心代码
swift
struct ContentView: View {
@State private var checkAmount = 0.0
@State private var numberOfPeople = 0
@State private var tipPercentage = 20
@FocusState private var amountIsFocused: Bool
private var totalPerPerson: Double {
let peopleCount = Double(numberOfPeople + 2)
let tipSelection = Double(tipPercentage)
return checkAmount * tipSelection / peopleCount / 100
}
let tipPercentages = [10, 15, 20, 25, 0]
var body: some View {
NavigationView {
Form {
Section("") {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
.keyboardType(.decimalPad)
.focused($amountIsFocused)
Picker("Number of people", selection: $numberOfPeople) {
ForEach(2..<100) {
Text("\($0) people")
}
}
}
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(tipPercentages, id: \.self) {
Text($0, format: .percent)
}
}
.pickerStyle(.segmented)
}header: {
Text("How much tip do you want to leave")
}
Section {
Text(totalPerPerson, format: .currency(code: Locale.current.currencyCode ?? "USD"))
}
}
.navigationTitle("WeSplit")
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
amountIsFocused = false
}
}
}
}
}
}
知识点
Form
TextField & format: .currency(code: Locale.current.currencyCode ?? "USD")
Picker
Section
toolbar修饰符
效果
GuessTheFlag
核心代码
swift
struct ContentView: View {
@State private var showScores = false
@State private var scoreTitle = ""
@State private var scores = 0
@State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria",
"Poland", "Russia", "Spain", "UK", "US"].shuffled()
@State var correctAnswer = Int.random(in: 0...2)
var body: some View {
ZStack {
//AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green,.blue, .purple,.red]), center: .center)
RadialGradient(stops: [
.init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
.init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3)
], center: .top, startRadius: 200, endRadius: 700)
.ignoresSafeArea()
VStack {
Spacer()
Text("Guess the flag")
.font(.largeTitle.weight(.bold))
.foregroundColor(.white)
VStack(spacing: 15) {
VStack{
Text("Tap the flag of")
.foregroundStyle(.secondary)
.font(.subheadline.weight(.heavy))
Text(countries[correctAnswer])
.font(.largeTitle.weight(.semibold))
}
ForEach(0..<3) {number in
Button {
flagTapper(number)
}label: {
Image(countries[number])
.renderingMode(.original)
.clipShape(Capsule())
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 20)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
Spacer()
Text("Score \(scores)")
.foregroundColor(.white)
.font(.title.bold())
Spacer()
}
.padding()
}
.alert(scoreTitle, isPresented: $showScores) {
Button("Continue", action: askQuestion)
} message: {
Text("Your score is \(scores)")
}
}
func flagTapper(_ number: Int) {
if number == correctAnswer {
scoreTitle = "Correct"
scores += 10
} else {
scoreTitle = "Wrong"
}
showScores = true
}
func askQuestion() {
countries.shuffle()
correctAnswer = Int.random(in: 0...2)
}
}
知识点
- Gradient
- alert
效果
BetterRest
核心代码
swift
import CoreML
import SwiftUI
struct ContentView: View {
@State private var wakeUp = defaultWakeTime
@State private var sleepAmount = 8.0
@State private var coffeeAmount = 1
@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var showingAlert = false
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
var body: some View {
NavigationView {
Form {
VStack(alignment: .leading, spacing: 0) {
Text("When do you want to wake up?")
.font(.headline)
DatePicker("Please enter atime", selection: $wakeUp, displayedComponents: .hourAndMinute)
.labelsHidden()
}
VStack(alignment: .leading, spacing: 0) {
Text("Desired amount of sleep")
.font(.headline)
Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Daily coffee intake")
Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount) cups", value: $coffeeAmount, in: 0...20)
}
}
.navigationTitle("BetterRest")
.toolbar {
Button("Calculate", action: calculateBedtime)
}
.alert(alertTitle, isPresented: $showingAlert) {
Button("OK") {}
}message: {
Text(alertMessage)
}
}
}
func calculateBedtime() {
do {
let config = MLModelConfiguration()
let model = try SleepCalculator(configuration: config)
let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
let hour = (components.hour ?? 0) * 60 * 60
let minute = (components.minute ?? 0) * 60
let prediction = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
let sleepTime = wakeUp - prediction.actualSleep
alertTitle = "Your ideal bedtime is ..."
alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
}catch {
alertTitle = "Error"
alertMessage = "Sorry, there was a problem calculating your bedtime."
}
showingAlert = true
}
}
知识点
CoreML机器学习
DateComponents、DatePicker
alert修饰符
Stepper
WordScramble
核心代码
swift
struct ContentView: View {
@State private var usedWords = [String]()
@State private var rootWord = ""
@State private var newWord = ""
@State private var errorTitle = ""
@State private var errorMessage = ""
@State private var showingError = false
var body: some View {
NavigationView {
List {
Section {
TextField("enter your word", text: $newWord)
.autocapitalization(.none)
}
Section {
ForEach(usedWords, id: \.self) { word in
HStack{
Image(systemName: "\(word.count).circle")
Text(word)
}
}
}
}
.navigationTitle(rootWord)
.onSubmit(addNewWord)
.onAppear(perform: startGame)
.alert(errorTitle, isPresented: $showingError) {
Button("OK", role: .cancel) {}
}message: {
Text(errorMessage)
}
}
}
func addNewWord() {
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
guard answer.count > 0 else { return }
guard isOriginal(word: answer) else {
wordError(title: "Word used already", message: "Be more original")
return
}
guard isPossible(word: answer) else {
wordError(title: "word not possible", message: "you can't spell that word from '\(rootWord)'")
return
}
guard isReal(word: answer) else {
wordError(title: "word not recognized", message: "you can't just make them up, you know!")
return
}
withAnimation{
usedWords.insert(answer, at: 0)
}
newWord = ""
}
func startGame() {
if let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
if let startWords = try? String(contentsOf: fileURL) {
let allWords = startWords.components(separatedBy: "\n")
rootWord = allWords.randomElement() ?? "silkworm"
return
}
}
fatalError("Could not load start.txt from bundle")
}
func isOriginal(word: String) -> Bool {
!usedWords.contains(word)
}
func isPossible(word: String) -> Bool {
var tempWord = rootWord
for letter in word {
if let pos = tempWord.firstIndex(of: letter) {
tempWord.remove(at: pos)
} else {
return false
}
}
return true
}
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return misspelledRange.location == NSNotFound
}
func wordError(title: String, message: String) {
errorTitle = title
errorMessage = message
showingError = true
}
}
知识点
TextField("enter your word", text: $newWord).autocapitalization(.none)
onSubmit
、onAppear
修饰符读取静态资源
swiftif let fileURL = Bundle.main.url(forResource: "start", withExtension: "txt") { if let startWords = try? String(contentsOf: fileURL) { let allWords = startWords.components(separatedBy: "\n") rootWord = allWords.randomElement() ?? "silkworm" return } } fatalError("Could not load start.txt from bundle")
UITextChecker()
效果
iExpense
核心代码
Expenses(ViewModel)
swift
class Expenses: ObservableObject {
@Published var items = [ExpenseItem]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let itemArr = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decodedItems = try? decoder.decode([ExpenseItem].self, from: itemArr) {
items = decodedItems
return
}
}
items = []
}
}
ExpenseItem(Model)
swift
struct ExpenseItem: Identifiable, Codable {
let name: String
let type: String
let amount: Double
let id = UUID()
}
AddView
swift
struct AddView: View {
@ObservedObject var expenses: Expenses
@State private var name = ""
@State private var type = "Personal"
@State private var amount = 0.0
@Environment(\.dismiss) var dismiss
let types = ["Business", "Personal"]
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
Picker("Type", selection: $type) {
ForEach(types, id: \.self) {
Text($0)
}
}
TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
.keyboardType(.decimalPad)
}
.navigationTitle("Add new expense")
.toolbar {
Button("Save") {
let item = ExpenseItem(name: name, type: type, amount: amount)
expenses.items.append(item)
dismiss()
}
}
}
}
}
ContentView
swift
struct ContentView: View {
@StateObject var expenses = Expenses()
@State private var showingAddExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.type)
.font(.subheadline)
}
Spacer()
Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "CNY"))
}
}
.onDelete(perform: removeItems)
}
.navigationTitle("iExpense")
.toolbar {
Button {
showingAddExpense = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showingAddExpense) {
AddView(expenses: expenses)
}
}
}
func removeItems(at offsets: IndexSet) {
expenses.items.remove(atOffsets: offsets)
}
}
知识点
MVVM
@StateObject、@Published、ObservableObject、@ObservedObject
UserDefaults(存储少量数据,常用于存储Preference)
JSONEncoder、JSONDecoder、Codable
属性观察器(willSet、didSet) 配合 UserDefaults保存数据
onDelete修饰符
sheet修饰符(弹出新页面)
swift.sheet(isPresented: $showingAddExpense) { AddView(expenses: expenses) }
@Environment(.dismiss) var dismiss(@Environment(keyPath) 可以读取系统环境数据,dismiss用于关闭当前展示页面)