#05 VB Helper_Part 2

Angel Shau
海大 SwiftUI iOS / Flutter App 程式設計
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)
}
}
TabView & NavigationView實作畫面
  • 加入 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)
}
}
search bar實作畫面
  • 使用 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")
}
}
UIActivityViewController實作畫面
  • 使用 Core Data 儲存資料,資料可儲存跟刪除

本系統利用Core Data儲存使用者喜歡的運動場館的基本資料,以利使用者日後重新查看。

MyFavData.xcdatamodel設定頁面

利用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 }
})
}
Gesture實作畫面(左)Drag(右)
  • 使用到 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
  • 使用到至少一個沒教過的功能技術
  1. 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()!
}

}
QRCode實作畫面

(5) 加分功能

--

--