SwiftUI 学习(二)

115 阅读4分钟

阅前引导

通过阅读本文章,可以了解一下内容:

  1. 实现简单的引导页面。
  2. 简单的动画效果,缩放,绘画图形。
  3. 在地图上标记点

功能简述

  1. 功能包含引导启动页面
  2. 招采、项目列表和详情
  3. 地图和项目标记
  4. 我的页面

主要功能功能实现

引导页面

截屏2022-11-27 下午6.56.28.png

@AppStorage是SwiftUI框架提供的一个属性包装器,设计初衷是创建一种在视图中保存和读取UserDefaults变量的快捷方法。@AppStorage在视图中的行为同@State很类似,其值变化时将导致与其依赖的视图无效并进行重新绘制。

这里我们使用@AppStorage来标记是否展示引导页面,另外我们给引导页面的图片增加一个动画效果, 当图片首次展现时,对图片进行缩放scaleEffect。右滑切换引导页面,点击体验按钮进入首页。

    @State private var isAnimating: Bool = false

    var openImageName: String

    var openImageTitle: String

    @AppStorage("isOnboarding") varisOnboarding: Bool?    
    VStack(spacing: 20) {
            Image(openImageName)
                .resizable()
                .scaledToFit()
                .scaleEffect(isAnimating ? 1.0 : 0.6)
                .cornerRadius(20)
            Button {
                isOnboarding = **false**
            } label: {
                HStack(spacing: 8) {
                    Text("立即体验")
                    Image(systemName: "arrow.right.circle")
                        .font(.largeTitle)
                }
                .padding(.horizontal, 16)
                .padding(.vertical, 10)
                        }
            .accentColor(.black)
            Text("RCC@\(openImageTitle)")
                .font(.title)
                .fontWeight(.semibold)
        } //: ZStack
        .onAppear{
            withAnimation(.easeOut(duration: 0.5)) {
                isAnimating = true
            }
        }
        .padding(.horizontal, 20)

列表和详情页面

截屏2022-11-27 下午7.15.42.png

该部分有三个view页面组成,item单条记录view,列表view,详情页view, 列表使用List组件和NavigationView组件,使用NavigationLink进行页面跳转

单条记录View

  struct ProjectItemView: View {
    let project: Project
    var body: some View {
        VStack(alignment: .leading, spacing: 15) {
            Text(project.project_name)
                .font(.title3)
                .fontWeight(.heavy)
                .lineLimit(1)
            HStack (alignment: .center, spacing: 10) {
                Text(project.category)
                    .font(.headline)
                    .foregroundColor(.blue)
                Text(project.city)
                    .font(.headline)
                    .foregroundColor(.blue)
                Spacer()
            }
            Text("公布时间:\(project.new_info_date)")
                .font(.headline)
                .foregroundColor(.blue)
        }
    }
}

list列表view

struct ProjectItemListView: View {
    @State var projects:[Project] = Bundle.main.decode("projects.json")
    var body: some View {
        NavigationView {
            List {
                ForEach(projects) {project **in**
                    NavigationLink(destination: ProjectDetailView(project: project)) {
                        ProjectItemView(project: project)
                            .padding(.vertical, 8)
                    }
                }
                .listStyle(InsetListStyle())
                .navigationTitle("工程信息")
                .navigationBarTitleDisplayMode(.inline)
            }
        }
    }
}

地图标记页面

截屏2022-11-27 下午7.36.03.png

地图页面部分使用MapKit框架,这样才能使用苹果系统提供的地图相关功能,添加一个region变量,类型是MKCoordinateRegion,必须提供两个重要的信息才能保证地图的正常运行,一是地图中心坐标,经度和维度,使用CLLocationCoordinate2D,二是地图的坐标区域,使用MKCoordinateSpan。

struct MapView: View {
    @State private var region: MKCoordinateRegion = {
        var mapCoordinates = CLLocationCoordinate2D(latitude: 39.8997255, longitude: 116.3743353)
        var mapZoomLevel = MKCoordinateSpan(latitudeDelta: 0.045, longitudeDelta: 0.045)
        var mapRegin = MKCoordinateRegion(center: mapCoordinates, span: mapZoomLevel)
        return mapRegin
    }()
    let locations: [Place] = Bundle.main.decode("places.json")
    var body: some View {
        Map(coordinateRegion: $region,annotationItems: locations) { item in
//            MapPin(coordinate: item.location, tint: .accentColor)
//            MapMarker(coordinate: item.location, tint: .accentColor)
              MapAnnotation(coordinate: item.location) {
                MapAnnotationView(loaction: item)
              }
        }
    }
}

在地图上遍历绘制元素,MapPin、MapMarker是使用系统默认的图标,MapAnnotation 是自定义展示标注。这里我们给标注增了个同心圆的放大消失效果。在ZStack中上层定义一个圆,下层再定义一个小一点的圆,让这个圆的大小随时间变大到和上层圆一样,但是透明度从1变到0,让其一直重复。

var loaction: Place
    @State private var animation: Double = 0.0
    var body: some View {
        ZStack {
            Circle()
                .frame(width: 34, height: 34, alignment: .center)
            Circle()
                .stroke(Color.accentColor,lineWidth: 2)
                .frame(width: 32, height: 32, alignment: .center)
                .scaleEffect(1 + CGFloat(animation))
                .opacity(1 - animation)
            Image("firm-head")
                .resizable()
                .scaledToFit()
                .frame(width: 32, height: 32, alignment: .center)
                .clipShape(Circle())
        } //: ZStack
        .onAppear{
            withAnimation (
                Animation.easeOut(duration: 2).repeatForever(autoreverses:false)
            ) {
                animation = 1
            }
        }
    }

我的页面

截屏2022-11-27 下午8.08.44.png

这个部分基础信息部分使用Form和Section结合,跳转和上面的列表页面一样。头像部分clipShape裁剪个圆形,并在上面覆盖一个宽带1的圆环。

            Image("skye")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 100)
                    .clipShape(Circle())
                    .overlay(
                        Circle().stroke(Color(.systemGray5), lineWidth: 1)
                    )

背景动画部分,GeometryReader是一个容器视图,可以根据其的自身大小和坐标空间定义其内容。图形绘制:圆随机个数,随机速度,随机缩放,随机尺寸,随机坐标,随机掩饰,stiffness 、damping设置阻尼效果,使用drawingGroup修饰器,属于Metal苹果系统高级框架,可以直接工作再GPU上,图形复杂不再影响性能问题。

@State private var randomCircle = Int.random(in: 12...16)
    @State private var isAnimation = false
    // 1. 随机坐标
    func randomCoordinate(max: CGFloat) -> CGFloat {
        return CGFloat.random(in: 0...max)
    }
    // 2. 随机尺寸
    func randomSize() -> CGFloat {
       return CGFloat.random(in: 10...300)
    }
    // 3.随机缩放比
    func randomScale() -> CGFloat {
        return CGFloat.random(in: 0.1...2.0)
    }
    // 4.随机速度
    func randomSpeed() -> Double {

        Double.random(in: 0.025...1.0)

    }

    // 5.随机掩饰
    func randomDelay() -> Double {
        Double.random(in: 0...2)
    }
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                ForEach( 0 ..< randomCircle, id: \.**self**) { item in
                    Circle()
                        .foregroundColor(.blue)
                        .opacity(0.15)
                        .frame(width: randomSize(), height: randomSize(), alignment: .center)
                        .scaleEffect(isAnimation ? randomScale() : 1)
                        .position(x: randomCoordinate(max: geometry.size.width), y: randomCoordinate(max: geometry.size.height))
                        .animation(
                            Animation.interpolatingSpring(stiffness: 0.5, damping: 0.5)
                                .repeatForever()
                                .speed(randomSpeed())
                                .delay(randomDelay())
                        )
                        .onAppear{
                            isAnimation = true
                        }
                }
            }
            .drawingGroup()
        }
    }

总结

  1. 学习到了如何在地图上添加标注的三种方式
  2. 学习了如何简单添加缩放的动画效果
  3. 学习了如何自定义绘制动画图形