SwiftUI小功能模块系列
0001、SwiftUI自定义Tabbar动画效果
0002、SwiftUI自定义3D动画导航抽屉效果
0003、SwiftUI搭建瀑布流-交错网格-效果
0004、SwiftUI-<探探App>喜欢手势卡片
技术:SwiftUI3.0、喜欢卡片、手势、探探App、TinderApp
运行环境:
SwiftUI3.0 + Xcode13.4.1 + MacOS12.5 + iPhone Simulator iPhone 13 Pro Max
使用SwiftUI做一个类似探探App的喜欢手势卡片
思路:
1.创建主窗口
2.搭建导航和底部按钮和中间卡片部分
3.添加卡片用户数据、自定义喜欢卡片UI
4.处理拖拽手势操作、左右滑动进行通知卡片页面删除卡片
颜色
Blue#8BBDE6
Gray#9A9D9F
Pink#E76397
Yellow#E7BB71
随机美女6-10张
New Group
命名为 View
New File
选择SwiftUI View
类型 命名为Home
New File
选择SwiftUI View
类型 命名为StackCardView
New Group
命名为 Model
New File
选择SwiftUI View
类型 命名为User
、并且删除预览视图、改造成模型 继承Identifiable
New Group
命名为 ViewModel
New File
选择SwiftUI View
类型 命名为HomeViewModel
、并且删除预览视图、改造成模型 继承ObservableObject
类型是class
主要是展示主窗口
Home
//
// ContentView.swift
// Shared
//
// Created by 李宇鸿 on 2022/8/13.
//
import SwiftUI
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
思路
- 创建导航
- 创建底部四个按钮
- 创建卡片模块
- 创建卡片数据、创建卡片UI
- 监听卡片没有情况 按钮不可点击
- 监听按钮和手势的操作卡片的通知传递
//
// Home.swift
// TanTanUI (iOS)
//
// Created by 李宇鸿 on 2022/8/14.
//
import SwiftUI
struct Home: View {
@StateObject var homeData : HomeViewModel = HomeViewModel()
var body: some View {
VStack{
Button{
}label: {
Image("menu")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 22, height: 22)
}
.frame(maxWidth:.infinity,alignment: .leading)
.overlay(
Text("Disover")
.font(.title.bold())
)
.foregroundColor(.black)
.padding()
// 用户布局
ZStack{
if let users = homeData.displaying_users{
if users.isEmpty{
Text("Come back later can find more matches for you!")
.font(.caption)
.foregroundColor(.gray)
}
else{
// 显示卡片……
//从Zstack开始反转牌
//你可以在这里使用反向操作…
//或者你可以在抓取用户时…
ForEach(users.reversed()){ user in
// 卡片视图
StackCardView(user: user)
.environmentObject(homeData)
}
}
}
else{
ProgressView()
}
}
.padding(.top,30)
.padding()
.padding(.vertical)
.frame(maxWidth:.infinity,maxHeight: .infinity)
// 多个按钮
HStack(spacing:15){
Button{
}label: {
Image(systemName: "arrow.uturn.backward")
.font(.system(size: 15,weight: .bold))
.foregroundColor(.white)
.shadow(radius: 5)
.padding(13)
.background(Color("Gray"))
.clipShape(Circle())
}
Button{
doSwipe()
}label: {
Image(systemName: "xmark")
.font(.system(size: 20,weight: .black))
.foregroundColor(.white)
.shadow(radius: 5)
.padding(13)
.background(Color("Blue"))
.clipShape(Circle())
}
Button{
}label: {
Image(systemName: "star.fill")
.font(.system(size: 15,weight: .bold))
.foregroundColor(.white)
.shadow(radius: 5)
.padding(13)
.background(Color.yellow)
.clipShape(Circle())
}
Button{
doSwipe(rightSwipe: true)
}label: {
Image(systemName: "suit.heart.fill")
.font(.system(size: 20,weight: .black))
.foregroundColor(.white)
.shadow(radius: 5)
.padding(13)
.background(Color("Pink"))
.clipShape(Circle())
}
}
.padding(.bottom)
// 设置按钮不可点击
.disabled(homeData.displaying_users?.isEmpty ?? false)
.opacity((homeData.displaying_users?.isEmpty ?? false ? 0.6 : 1))
}
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .top)
}
// removing cards when doing Swipe...
// 在刷牌时移除卡片…
func doSwipe(rightSwipe: Bool = false){
guard let first = homeData.displaying_users?.first else{
return
}
// Using Notification to post and receiving in Stack Cards...
// 使用通知在堆叠卡片中寄收……
NotificationCenter.default.post(name: NSNotification.Name("ACTIONROMBUTTON"), object: nil,userInfo: [
"id": first.id,
"rightSwipe": rightSwipe
])
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
主要是做 卡片UI展示
思路
- 搭建卡片UI
- 监听上层传递的通知手势处理拖拽操作 进行监听是否删除卡片
- 拖拽卡片动画设计
//
// StackCardView.swift
// TanTanUI (iOS)
//
// Created by 李宇鸿 on 2022/8/15.
//
import SwiftUI
struct StackCardView: View {
@EnvironmentObject var homeData: HomeViewModel
var user: User
// 手势属性……
@State var offset : CGFloat = 0;
@GestureState var isDragging : Bool = false
@State var endSwipe : Bool = false
var body: some View {
GeometryReader {proxy in
let size = proxy.size
let index = CGFloat(homeData.getIndex(user: user))
// 显示下两个卡在顶部像一个堆栈....
let topOffset = (index <= 2 ? index : 2 ) * 15
ZStack{
Image(user.profilePic)
.resizable()
.aspectRatio(contentMode: .fill)
// 减少宽度
.frame(width: size.width - topOffset, height: size.height)
.cornerRadius(15)
.offset(y:-topOffset)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
.offset(x:offset)
.rotationEffect(.init(degrees: getRotation(angle: 8)))
.contentShape(Rectangle().trim(from: 0, to: endSwipe ? 0 : 1))
.gesture(
DragGesture()
.updating($isDragging, body: { value, out, _ in
out = true
})
.onChanged({ value in
let translation = value.translation.width
offset = (isDragging ? translation : .zero)
})
.onEnded({ vlaue in
let width = getRect().width
let translation = vlaue.translation.width
let checkingStatus = (translation > 0 ? translation : -translation)
withAnimation {
if checkingStatus > (width / 2){
// 删除卡片
offset = (translation > 0 ? width : -width) * 2
endSwipeActions()
if translation > 0 {
rightSwipe()
}
else {
leftSwipe()
}
}
else
{
// reset...
// 重置
offset = .zero
}
}
})
)
// 接收通知发布....
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("ACTIONROMBUTTON"),object:nil)) { data in
guard let info = data.userInfo else{
return
}
let id = info["id"] as? String ?? ""
let rightSwipe = info["rightSwipe"] as? Bool ?? false
let width = getRect().width - 50
if user.id == id {
// 删除卡片
withAnimation{
offset = (rightSwipe ? width : -width) * 2
endSwipeActions()
if rightSwipe {
self.rightSwipe()
}
else {
leftSwipe()
}
}
}
}
}
// 旋转
func getRotation(angle: Double)-> Double{
let rotation = (offset / (getRect().width - 50)) * angle
return rotation
}
func endSwipeActions(){
withAnimation(.none) {endSwipe = true}
//移走卡后,将卡从数组中移除以保存内存…
//根据你的动画持续时间延迟时间…
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if let _ = homeData.displaying_users?.first{
let _ = withAnimation {
homeData.displaying_users?.removeFirst()
}
}
}
}
func leftSwipe() {
// 在这里做动作
print("Left Swiped")
}
func rightSwipe() {
// 在这里做动作
print("right Swiped")
}
}
struct StackCardView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// 扩展视图获得边界…
extension View{
func getRect()->CGRect{
return UIScreen.main.bounds
}
}
用户模型
import SwiftUI
// 用户模型
struct User: Identifiable {
var id = UUID().uuidString
var name : String
var place : String
var profilePic : String
}
视图模型
- 处理创建用户数据
- 是否还有用户数据展示
- 获取当前图片的索引
import SwiftUI
// 视图模型,它保存所有用户数据…
class HomeViewModel: ObservableObject {
//存储所有获取的用户在这里…
//因为我们正在构建UI,所以在这里使用示例用户…
@Published var feched_users : [User] = []
@Published var displaying_users : [User]?
init(){
// 获取用户……
feched_users = [
User(name: "Natalia", place: "Vadlia NYC", profilePic: "User1"),
User(name: "Elisa", place: "Central NYC", profilePic: "User2"),
User(name: "Jasmine", place: "Metropolitan Museum NYC", profilePic: "User3"),
User(name: "Zahra", place: "Liberty NYC", profilePic: "User4"),
User(name: "Angelina", place: "Empier State NYC", profilePic: "User5"),
User(name: "Brittany", place: "Time Squart NYC", profilePic: "User6")
]
//存储在显示用户…
//显示用户的是什么?
//它将根据用户交互来更新/删除,以减少内存使用…
//同时我们需要所有获取的用户数据…
displaying_users = feched_users
}
// 检索索引…
func getIndex(user: User)->Int{
let index = displaying_users?.firstIndex(where: { currentUser in
return user.id == currentUser.id
}) ?? 0
return index
}
}