版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2020.03.30 星期一 |
前言
今天翻閱蘋果的API文檔傲绣,發(fā)現(xiàn)多了一個框架SwiftUI,這里我們就一起來看一下這個框架。感興趣的看下面幾篇文章脆丁。
1. SwiftUI框架詳細解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細解析 (四) —— 使用SwiftUI進行蘋果登錄(一)
5. SwiftUI框架詳細解析 (五) —— 使用SwiftUI進行蘋果登錄(二)
6. SwiftUI框架詳細解析 (六) —— 基于SwiftUI的導航的實現(xiàn)(一)
7. SwiftUI框架詳細解析 (七) —— 基于SwiftUI的導航的實現(xiàn)(二)
8. SwiftUI框架詳細解析 (八) —— 基于SwiftUI的動畫的實現(xiàn)(一)
9. SwiftUI框架詳細解析 (九) —— 基于SwiftUI的動畫的實現(xiàn)(二)
10. SwiftUI框架詳細解析 (十) —— 基于SwiftUI構建各種自定義圖表(一)
11. SwiftUI框架詳細解析 (十一) —— 基于SwiftUI構建各種自定義圖表(二)
12. SwiftUI框架詳細解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)
源碼
1. Swift
首先看下工程組織結構
下面就是源碼了
1. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
}
2. SceneDelegate.swift
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
@Published var mesh = Mesh.sampleMesh()
@Published var selection = SelectionHandler()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = SurfaceView(mesh: mesh, selection: selection)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
3. CGPoint+Help.swift
import CoreGraphics
extension CGPoint {
func translatedBy(x: CGFloat, y: CGFloat) -> CGPoint {
return CGPoint(x: self.x + x, y: self.y + y)
}
}
extension CGPoint {
func alignCenterInParent(_ parent: CGSize) -> CGPoint {
let x = parent.width/2 + self.x
let y = parent.height/2 + self.y
return CGPoint(x: x, y: y)
}
func scaledFrom(_ factor: CGFloat) -> CGPoint {
return CGPoint(
x: self.x * factor,
y: self.y * factor)
}
}
extension CGSize {
func scaledDownTo(_ factor: CGFloat) -> CGSize {
return CGSize(width: width/factor, height: height/factor)
}
var length: CGFloat {
return sqrt(pow(width, 2) + pow(height, 2))
}
var inverted: CGSize {
return CGSize(width: -width, height: -height)
}
}
4. Edge.swift
import Foundation
import CoreGraphics
typealias EdgeID = UUID
struct Edge: Identifiable {
var id = EdgeID()
var start: NodeID
var end: NodeID
}
struct EdgeProxy: Identifiable {
var id: EdgeID
var start: CGPoint
var end: CGPoint
}
extension Edge {
static func == (lhs: Edge, rhs: Edge) -> Bool {
return lhs.start == rhs.start && lhs.end == rhs.end
}
}
5. Mesh.swift
import Foundation
import CoreGraphics
class Mesh: ObservableObject {
let rootNodeID: NodeID
@Published var nodes: [Node] = []
@Published var editingText: String
init() {
self.editingText = ""
let root = Node(text: "root")
rootNodeID = root.id
addNode(root)
}
var edges: [Edge] = [] {
didSet {
rebuildLinks()
}
}
@Published var links: [EdgeProxy] = []
func rebuildLinks() {
links = edges.compactMap { edge in
let snode = nodes.filter({ $0.id == edge.start }).first
let enode = nodes.filter({ $0.id == edge.end }).first
if let snode = snode, let enode = enode {
return EdgeProxy(id: edge.id, start: snode.position, end: enode.position)
}
return nil
}
}
func rootNode() -> Node {
guard let root = nodes.filter({ $0.id == rootNodeID }).first else {
fatalError("mesh is invalid - no root")
}
return root
}
func nodeWithID(_ nodeID: NodeID) -> Node? {
return nodes.filter({ $0.id == nodeID }).first
}
func replace(_ node: Node, with replacement: Node) {
var newSet = nodes.filter { $0.id != node.id }
newSet.append(replacement)
nodes = newSet
}
}
extension Mesh {
func updateNodeText(_ srcNode: Node, string: String) {
var newNode = srcNode
newNode.text = string
replace(srcNode, with: newNode)
}
func positionNode(_ node: Node, position: CGPoint) {
var movedNode = node
movedNode.position = position
replace(node, with: movedNode)
rebuildLinks()
}
func processNodeTranslation(_ translation: CGSize, nodes: [DragInfo]) {
nodes.forEach { draginfo in
if let node = nodeWithID(draginfo.id) {
let nextPosition = draginfo.originalPosition.translatedBy(x: translation.width, y: translation.height)
self.positionNode(node, position: nextPosition)
}
}
}
}
extension Mesh {
func addNode(_ node: Node) {
nodes.append(node)
}
func connect(_ parent: Node, to child: Node) {
let newedge = Edge(start: parent.id, end: child.id)
let exists = edges.contains(where: { edge in
return newedge == edge
})
guard exists == false else {
return
}
edges.append(newedge)
}
}
extension Mesh {
@discardableResult func addChild(_ parent: Node, at point: CGPoint? = nil) -> Node {
let target = point ?? parent.position
let child = Node(position: target, text: "child")
addNode(child)
connect(parent, to: child)
rebuildLinks()
return child
}
@discardableResult func addSibling(_ node: Node) -> Node? {
guard node.id != rootNodeID else {
return nil
}
let parentedges = edges.filter({ $0.end == node.id })
if
let parentedge = parentedges.first,
let parentnode = nodeWithID(parentedge.start) {
let sibling = addChild(parentnode)
return sibling
}
return nil
}
func deleteNodes(_ nodesToDelete: [NodeID]) {
for id in nodesToDelete where id != rootNodeID {
if let delete = nodes.firstIndex(where: { $0.id == id }) {
nodes.remove(at: delete)
let remainingEdges = edges.filter({ $0.end != id && $0.start != id })
edges = remainingEdges
}
}
rebuildLinks()
}
func deleteNodes(_ nodesToDelete: [Node]) {
deleteNodes(nodesToDelete.map({ $0.id }))
}
}
extension Mesh {
func locateParent(_ node: Node) -> Node? {
let parentedges = edges.filter { $0.end == node.id }
if let parentedge = parentedges.first,
let parentnode = nodeWithID(parentedge.start) {
return parentnode
}
return nil
}
func distanceFromRoot(_ node: Node, distance: Int = 0) -> Int? {
if node.id == rootNodeID { return distance }
if let ancestor = locateParent(node) {
if ancestor.id == rootNodeID {
return distance + 1
} else {
return distanceFromRoot(ancestor, distance: distance + 1)
}
}
return nil
}
}
6. Mesh+Demo.swift
import Foundation
import CoreGraphics
extension Mesh {
static func sampleMesh() -> Mesh {
let mesh = Mesh()
mesh.updateNodeText(mesh.rootNode(), string: "every human has a right to")
[(0, "shelter"),
(120, "food"),
(240, "education")].forEach { (angle, name) in
let point = mesh.pointWithCenter(center: .zero, radius: 200, angle: angle.radians)
let node = mesh.addChild(mesh.rootNode(), at: point)
mesh.updateNodeText(node, string: name)
}
return mesh
}
static func sampleProceduralMesh() -> Mesh {
let mesh = Mesh()
//seed root node with 3 children
[0, 1, 2, 3].forEach { index in
let point = mesh.pointWithCenter(center: .zero, radius: 400, angle: (index * 90 + 30).radians)
let node = mesh.addChild(mesh.rootNode(), at: point)
mesh.updateNodeText(node, string: "A\(index + 1)")
mesh.addChildrenRecursive(to: node, distance: 200, generation: 1)
}
return mesh
}
func addChildrenRecursive(to node: Node, distance: CGFloat, generation: Int) {
let labels = ["A", "B", "C", "D", "E", "F"]
guard generation < labels.count else {
return
}
let childCount = Int.random(in: 1..<4)
var count = 0
while count < childCount {
count += 1
let position = positionForNewChild(node, length: distance)
let child = addChild(node, at: position)
updateNodeText(child, string: "\(labels[generation])\(count + 1)")
addChildrenRecursive(to: child, distance: distance + 200.0, generation: generation + 1)
}
}
}
extension Int {
var radians: CGFloat {
CGFloat(self) * CGFloat.pi/180.0
}
}
7. Mesh+MathHelp.swift
import Foundation
import CoreGraphics
extension Mesh {
public func positionForNewChild(_ parent: Node, length: CGFloat) -> CGPoint {
let childEdges = edges.filter { $0.start == parent.id }
if let grandparentedge = edges.filter({ $0.end == parent.id }).first, let grandparent = nodeWithID(grandparentedge.start) {
let baseAngle = angleFrom(start: grandparent.position, end: parent.position)
let childBasedAngle = positionForChildAtIndex(childEdges.count, baseAngle: baseAngle)
let newpoint = pointWithCenter(center: parent.position, radius: length, angle: childBasedAngle)
return newpoint
}
return CGPoint(x: 200, y: 200)
}
/// get angle for n'th child in order delta * 0,1,-1,2,-2
func positionForChildAtIndex(_ index: Int, baseAngle: CGFloat) -> CGFloat {
let jitter = CGFloat.random(in: CGFloat(-1.0)...CGFloat(1.0)) * CGFloat.pi/32.0
guard index > 0 else { return baseAngle + jitter }
let level = (index + 1)/2
let polarity: CGFloat = index % 2 == 0 ? -1.0:1.0
let delta = CGFloat.pi/6.0 + jitter
return baseAngle + polarity * delta * CGFloat(level)
}
/// angle in radians
func pointWithCenter(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let deltax = radius*cos(angle)
let deltay = radius*sin(angle)
let newpoint = CGPoint(x: center.x + deltax, y: center.y + deltay)
return newpoint
}
func angleFrom(start: CGPoint, end: CGPoint) -> CGFloat {
var deltax = end.x - start.x
let deltay = end.y - start.y
if abs(deltax) < 0.001 {
deltax = 0.001
}
let angle = atan(deltay/abs(deltax))
return deltax > 0 ? angle: CGFloat.pi - angle
}
}
8. Node.swift
import Foundation
import CoreGraphics
typealias NodeID = UUID
struct Node: Identifiable {
var id: NodeID = NodeID()
var position: CGPoint = .zero
var text: String = ""
var visualID: String {
return id.uuidString
+ "\(text.hashValue)"
}
}
extension Node {
static func == (lhs: Node, rhs: Node) -> Bool {
return lhs.id == rhs.id
}
}
9. SelectionHandler.swift
import Foundation
import CoreGraphics
struct DragInfo {
var id: NodeID
var originalPosition: CGPoint
}
class SelectionHandler: ObservableObject {
@Published var draggingNodes: [DragInfo] = []
@Published private(set) var selectedNodeIDs: [NodeID] = []
@Published var editingText: String = ""
func selectNode(_ node: Node) {
selectedNodeIDs = [node.id]
editingText = node.text
}
func isNodeSelected(_ node: Node) -> Bool {
return selectedNodeIDs.contains(node.id)
}
func selectedNodes(in mesh: Mesh) -> [Node] {
return selectedNodeIDs.compactMap { mesh.nodeWithID($0) }
}
func onlySelectedNode(in mesh: Mesh) -> Node? {
let selectedNodes = self.selectedNodes(in: mesh)
if selectedNodes.count == 1 {
return selectedNodes.first
}
return nil
}
func startDragging(_ mesh: Mesh) {
draggingNodes = selectedNodes(in: mesh)
.map { DragInfo(id: $0.id, originalPosition: $0.position) }
}
func stopDragging(_ mesh: Mesh) {
draggingNodes = []
}
}
10. BoringListView.swift
import SwiftUI
struct BoringListView: View {
@ObservedObject var mesh: Mesh
@ObservedObject var selection: SelectionHandler
func indent(_ node: Node) -> CGFloat {
let base = 20.0
return CGFloat(mesh.distanceFromRoot(node) ?? 0) * CGFloat(base)
}
var body: some View {
List(mesh.nodes, id: \.id) { node in
Text(node.text)
.padding(EdgeInsets(
top: 0,
leading: self.indent(node),
bottom: 0,
trailing: 0))
}
}
}
struct BoringListView_Previews: PreviewProvider {
static var previews: some View {
let mesh = Mesh.sampleMesh()
let selection = SelectionHandler()
return BoringListView(mesh: mesh, selection: selection)
}
}
11. NodeView.swift
import SwiftUI
struct NodeView: View {
static let width = CGFloat(100)
// 1
@State var node: Node
//2
@ObservedObject var selection: SelectionHandler
//3
var isSelected: Bool {
return selection.isNodeSelected(node)
}
var body: some View {
Ellipse()
.fill(Color.green)
.overlay(Ellipse()
.stroke(isSelected ? Color.red : Color.black, lineWidth: isSelected ? 5 : 3))
.overlay(Text(node.text)
.multilineTextAlignment(.center)
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)))
.frame(width: NodeView.width, height: NodeView.width, alignment: .center)
}
}
struct NodeView_Previews: PreviewProvider {
static var previews: some View {
let selection1 = SelectionHandler()
let node1 = Node(text: "hello world")
let selection2 = SelectionHandler()
let node2 = Node(text: "I'm selected, look at me")
selection2.selectNode(node2)
return VStack {
NodeView(node: node1, selection: selection1)
NodeView(node: node2, selection: selection2)
}
}
}
12. EdgeView.swift
import SwiftUI
typealias AnimatablePoint = AnimatablePair<CGFloat, CGFloat>
typealias AnimatableCorners = AnimatablePair<AnimatablePoint, AnimatablePoint>
struct EdgeView: Shape {
var startx: CGFloat = 0
var starty: CGFloat = 0
var endx: CGFloat = 0
var endy: CGFloat = 0
// 1
init(edge: EdgeProxy) {
// 2
startx = edge.start.x
starty = edge.start.y
endx = edge.end.x
endy = edge.end.y
}
// 3
func path(in rect: CGRect) -> Path {
var linkPath = Path()
linkPath.move(to: CGPoint(x: startx, y: starty)
.alignCenterInParent(rect.size))
linkPath.addLine(to: CGPoint(x: endx, y:endy)
.alignCenterInParent(rect.size))
return linkPath
}
var animatableData: AnimatableCorners {
get { AnimatablePair(
AnimatablePair(startx, starty),
AnimatablePair(endx, endy))
}
set {
startx = newValue.first.first
starty = newValue.first.second
endx = newValue.second.first
endy = newValue.second.second
}
}
}
struct EdgeView_Previews: PreviewProvider {
static var previews: some View {
let edge1 = EdgeProxy(
id: UUID(),
start: CGPoint(x: -100, y: -100),
end: CGPoint(x: 100, y: 100))
let edge2 = EdgeProxy(
id: UUID(),
start: CGPoint(x: 100, y: -100),
end: CGPoint(x: -100, y: 100))
return ZStack {
EdgeView(edge: edge1).stroke(lineWidth: 4)
EdgeView(edge: edge2).stroke(Color.blue, lineWidth: 2)
}
}
13. NodeMapView.swift
import SwiftUI
struct NodeMapView: View {
@ObservedObject var selection: SelectionHandler
@Binding var nodes: [Node]
var body: some View {
ZStack {
ForEach(nodes, id: \.visualID) { node in
NodeView(node: node, selection: self.selection)
.offset(x: node.position.x, y: node.position.y)
.onTapGesture {
self.selection.selectNode(node)
}
}
}
}
}
struct NodeMapView_Previews: PreviewProvider {
static let node1 = Node(position: CGPoint(x: -100, y: -30), text: "hello")
static let node2 = Node(position: CGPoint(x: 100, y: 30), text: "world")
@State static var nodes = [node1, node2]
static var previews: some View {
let selection = SelectionHandler()
return NodeMapView(selection: selection, nodes: $nodes)
}
}
14. EdgeMapView.swift
import SwiftUI
struct EdgeMapView: View {
@Binding var edges: [EdgeProxy]
var body: some View {
ZStack {
ForEach(edges) { edge in
EdgeView(edge: edge)
.stroke(Color.black, lineWidth: 3.0)
}
}
}
}
struct EdgeMapView_Previews: PreviewProvider {
static let proxy1 = EdgeProxy(
id: EdgeID(),
start: .zero,
end: CGPoint(x: -100, y: 30))
static let proxy2 = EdgeProxy(
id: EdgeID(),
start: .zero,
end: CGPoint(x: 100, y: 30))
@State static var edges = [proxy1, proxy2]
static var previews: some View {
EdgeMapView(edges: $edges)
}
}
15. MapView.swift
import SwiftUI
struct MapView: View {
@ObservedObject var selection: SelectionHandler
@ObservedObject var mesh: Mesh
var body: some View {
ZStack {
Rectangle().fill(Color.orange)
EdgeMapView(edges: $mesh.links)
NodeMapView(selection: selection, nodes: $mesh.nodes)
}
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
let mesh = Mesh()
let child1 = Node(position: CGPoint(x: 100, y: 200), text: "child 1")
let child2 = Node(position: CGPoint(x: -100, y: 200), text: "child 2")
[child1, child2].forEach {
mesh.addNode($0)
mesh.connect(mesh.rootNode(), to: $0)
}
mesh.connect(child1, to: child2)
let selection = SelectionHandler()
return MapView(selection: selection, mesh: mesh)
}
}
16. SurfaceView.swift
import SwiftUI
struct SurfaceView: View {
@ObservedObject var mesh: Mesh
@ObservedObject var selection: SelectionHandler
//dragging
@State var portalPosition: CGPoint = .zero
@State var dragOffset: CGSize = .zero
@State var isDragging: Bool = false
@State var isDraggingMesh: Bool = false
//zooming
@State var zoomScale: CGFloat = 1.0
@State var initialZoomScale: CGFloat?
@State var initialPortalPosition: CGPoint?
var body: some View {
VStack {
// 1
Text("drag offset = w:\(dragOffset.width), h:\(dragOffset.height)")
Text("portal offset = x:\(portalPosition.x), y:\(portalPosition.y)")
Text("zoom = \(zoomScale)")
TextField("Breathe…", text: $selection.editingText, onCommit: {
if let node = self.selection.onlySelectedNode(in: self.mesh) {
self.mesh.updateNodeText(node, string: self.self.selection.editingText)
}
})
// 2
GeometryReader { geometry in
// 3
ZStack {
Rectangle().fill(Color.yellow)
MapView(selection: self.selection, mesh: self.mesh)
.scaleEffect(self.zoomScale)
// 4
.offset(
x: self.portalPosition.x + self.dragOffset.width,
y: self.portalPosition.y + self.dragOffset.height)
.animation(.easeIn)
}
.gesture(DragGesture()
.onChanged { value in
self.processDragChange(value, containerSize: geometry.size)
}
.onEnded { value in
self.processDragEnd(value)
})
.gesture(MagnificationGesture()
.onChanged { value in
// 1
if self.initialZoomScale == nil {
self.initialZoomScale = self.zoomScale
self.initialPortalPosition = self.portalPosition
}
self.processScaleChange(value)
}
.onEnded { value in
// 2
self.processScaleChange(value)
self.initialZoomScale = nil
self.initialPortalPosition = nil
})
}
}
}
}
struct SurfaceView_Previews: PreviewProvider {
static var previews: some View {
let mesh = Mesh.sampleProceduralMesh()
let selection = SelectionHandler()
return SurfaceView(mesh: mesh, selection: selection)
}
}
private extension SurfaceView {
// 1
func distance(from pointA: CGPoint, to pointB: CGPoint) -> CGFloat {
let xdelta = pow(pointA.x - pointB.x, 2)
let ydelta = pow(pointA.y - pointB.y, 2)
return sqrt(xdelta + ydelta)
}
// 2
func hitTest(point: CGPoint, parent: CGSize) -> Node? {
for node in mesh.nodes {
let endPoint = node.position
.scaledFrom(zoomScale)
.alignCenterInParent(parent)
.translatedBy(x: portalPosition.x, y: portalPosition.y)
let dist = distance(from: point, to: endPoint) / zoomScale
//3
if dist < NodeView.width / 2.0 {
return node
}
}
return nil
}
// 4
func processNodeTranslation(_ translation: CGSize) {
guard !selection.draggingNodes.isEmpty else { return }
let scaledTranslation = translation.scaledDownTo(zoomScale)
mesh.processNodeTranslation(
scaledTranslation,
nodes: selection.draggingNodes)
}
func processDragChange(_ value: DragGesture.Value, containerSize: CGSize) {
// 1
if !isDragging {
isDragging = true
if let node = hitTest(
point: value.startLocation,
parent: containerSize) {
isDraggingMesh = false
selection.selectNode(node)
// 2
selection.startDragging(mesh)
} else {
isDraggingMesh = true
}
}
// 3
if isDraggingMesh {
dragOffset = value.translation
} else {
processNodeTranslation(value.translation)
}
}
// 4
func processDragEnd(_ value: DragGesture.Value) {
isDragging = false
dragOffset = .zero
if isDraggingMesh {
portalPosition = CGPoint(
x: portalPosition.x + value.translation.width,
y: portalPosition.y + value.translation.height)
} else {
processNodeTranslation(value.translation)
selection.stopDragging(mesh)
}
}
// 1
func scaledOffset(_ scale: CGFloat, initialValue: CGPoint) -> CGPoint {
let newx = initialValue.x*scale
let newy = initialValue.y*scale
return CGPoint(x: newx, y: newy)
}
func clampedScale(_ scale: CGFloat, initialValue: CGFloat?) -> (scale: CGFloat, didClamp: Bool) {
let minScale: CGFloat = 0.1
let maxScale: CGFloat = 2.0
let raw = scale.magnitude * (initialValue ?? maxScale)
let value = max(minScale, min(maxScale, raw))
let didClamp = raw != value
return (value, didClamp)
}
func processScaleChange(_ value: CGFloat) {
let clamped = clampedScale(value, initialValue: initialZoomScale)
zoomScale = clamped.scale
if !clamped.didClamp,
let point = initialPortalPosition {
portalPosition = scaledOffset(value, initialValue: point)
}
}
}
后記
本篇主要講述了
Mind-Map UI
,感興趣的給個贊或者關注~~~