#05 VB Helper_Part 2
Published in
14 min readJan 26, 2022
找不到地方打球嗎?
難以選擇場地嗎?
VB Helper幫你尋找、
紀錄喜歡的場地!!
(1) Demo影片
(2) GitHub連結
(3) 作品介紹
這是一個提供給排球迷迷們的小幫手~
(4) 功能需求
- 以 TabView & NavigationView 製作多頁面 App
利用Tabview製作分頁
struct MainPageView: View { var body: some View {
TabView { CoverPageView()
.tabItem {
Label("首頁", systemImage: "house.fill")
} GymListView()
.tabItem {
Label("尋找場館", systemImage: "map.fill")
} MyFavGymListView()
.tabItem {
Label("我的最愛", systemImage: "heart.fill")
}
}
}
}
利用NavigationView製作分頁
var body: some View { NavigationView { ScrollView(.vertical) {
Spacer().frame(height: 50)
let columns = [GridItem()]
LazyVGrid(columns: columns) {
if !gymListViewModel.gymList.isEmpty {
ForEach(gymListViewModel.gymList) { gym in NavigationLink {
GymDetailPageView(gymInfo: gym)
} label: { GymInfoView(gymInfo: gym) } }
}
else { ProgressView("Searching...") }
}
}
.navigationBarTitle("體育場館", displayMode: .inline)
.navigationBarColor(backgroundColor: UIColor(red: 85/255, green: 111/255, blue: 122/255, alpha: 1), titleColor: UIColor.white)
}
.foregroundColor(.black)
.searchable(text: $searchKey, placement: .navigationBarDrawer(displayMode: .always))
.onAppear {
if gymListViewModel.gymList.isEmpty {
gymListViewModel.fetchGymInfo(city: searchKey)
print(gymListViewModel.gymList)
}
}
.onChange(of: searchKey) { sKey in
gymListViewModel.fetchGymInfo(city: sKey)
}
}
- 加入 search 功能
在場館搜尋頁面加入search bar。
var body: some View { NavigationView {
...
}
.navigationBarTitle("體育場館", displayMode: .inline)
.navigationBarColor(backgroundColor: UIColor(red: 85/255, green: 111/255, blue: 122/255, alpha: 1), titleColor: UIColor.white)
}
.foregroundColor(.black)
.searchable(text: $searchKey, placement: .navigationBarDrawer(displayMode: .always))
.onAppear {
if gymListViewModel.gymList.isEmpty {
gymListViewModel.fetchGymInfo(city: searchKey)
print(gymListViewModel.gymList)
}
}
.onChange(of: searchKey) { sKey in
gymListViewModel.fetchGymInfo(city: sKey)
}
}
- 使用 UIViewControllerRepresentable 加入 UIActivityViewController 實現分享功能。
此功能實作於本系統的運動場館詳細內容畫面,點擊Website按鈕之後可以選擇以瀏覽器開啟網頁或者分享連結。
Menu {
Button(action: {
guard let data = URL(string: webpage!) else { return }
let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?
.present(av, animated: true, completion: nil)
}, label: {
HStack {
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text("分享連結")
}
})
Link(destination: URL(string: urlEncoder(url: webpage))!
, label: {
HStack {
Image(systemName: "safari")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text("以瀏覽器開啟")
}
})
} label: {
HStack {
Image(systemName: "link")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text("Website")
}
}
- 使用 Core Data 儲存資料,資料可儲存跟刪除
本系統利用Core Data儲存使用者喜歡的運動場館的基本資料,以利使用者日後重新查看。
利用PersistenceController管理Core Data
struct PersistenceController { static let shared = PersistenceController()//測資
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for idx in 0..<5 {
let newItem = MyFav(context: viewContext)
newItem.timestamp = Date()
newItem.gymName = "排球場\(String(idx))"
}
do { try viewContext.save() }
catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()//Storage for Core Data let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyFavData")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores{ dscrp, err in
if let error = err {
fatalError("Error: \(error.localizedDescription)")
}
}
}//check whether the context has changed func save() {
let context = container.viewContext
if context.hasChanges {
do { try context.save() }
catch { print(error) }
}
}
}
- 使用動畫
本系統有兩處使用動畫,首先為封面的旋轉排球,另外是detail頁面中預覽無障礙設施圖片的視窗。
Volleyball
Image("vb")
.resizable()
.scaledToFit()
.rotationEffect(.degrees(rotateDrgree))
.animation(
.linear(duration: 0.1)
.repeatForever(autoreverses: false),
value: rotateDrgree)
.frame(width: 45)
.offset(x:28, y: -224)
.onAppear { rotateDrgree = 360 }
.gesture(LongPress)
Image Preview sheet
Group {
if isShowing { bodyContet }
}
.animation(.default)
.onReceive(Just(isShowing), perform: { isShowing in
offset = isShowing ? 0 : heightToDisappear
})
- 使用 Gesture (不包含 TapGesture)
本系統使用兩種手勢。一、LongPressGesture:於封面分頁中的排球,長按會出現有趣的迷因。二、DragGesture:於運動場館detail頁面中,取消預覽無障礙圖片可以用向下拖拽的方式關閉sheet視窗。
LongPressGesture
var LongPress: some Gesture {
LongPressGesture(minimumDuration: 5)
.updating($isDetectingLongPress) { currentState, gestureState, transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
self.completedLongPress = finished
}
}
DragGesture
var interactiveGesture: some Gesture {
DragGesture()
.onChanged({ (value) in
if value.translation.height > 0 {
offset = value.location.y
}
})
.onEnded({ (value) in
let diff = abs(offset-value.location.y)
if diff > 100 { hide() }
else { offset = 0 }
})
}
- 使用到 EnvironmentObject
利用EnvironmentObeject更新當前QRCode Generator所需要的資訊。必紀錄現在是否展示QRCode圖片。
struct GymMainDetailView: View { @EnvironmentObject var qrCodeInfo: QRCodeInfo
...
var body: some View {
VStack(alignment: .leading) {
VStack {
...
//分享、加入最愛按鈕
HStack {
Spacer()
Menu {
...
Button(action: {
qrCodeInfo.text = urlEncoder(url: webpage)
qrCodeInfo.display.toggle()
}, label: { ... })
}
}
...
}
- 使用 WidgetKit 製作 widget
- 使用到至少一個沒教過的功能技術
- QRCode Generater
struct QRCodeGeneratorView: View {
@State var targetURL: String = "test" var body: some View {
Image(uiImage: UIImage(data: getQRCode(url: targetURL)!)!)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
} //產生QRCode圖片
func getQRCode(url: String) -> Data? {
let data = url.data(using: .ascii, allowLossyConversion: false)
guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
filter.setValue(data, forKey: "inputMessage")
guard let ciimage = filter.outputImage else { return nil }
let transform = CGAffineTransform(scaleX: 10, y: 10)
let scaledCIImage = ciimage.transformed(by: transform)
let uiimage = UIImage(ciImage: scaledCIImage)
return uiimage.pngData()!
}
}