版本記錄
版本號 | 時(shí)間 |
---|---|
V1.0 | 2020.03.30 星期一 |
前言
今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動畫的實(shí)現(xiàn)(一)
9. SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動畫的實(shí)現(xiàn)(二)
10. SwiftUI框架詳細(xì)解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)
11. SwiftUI框架詳細(xì)解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)
12. SwiftUI框架詳細(xì)解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)
源碼
1. Swift
首先看下工程組織結(jié)構(gòu)
下面就是源碼了
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
,感興趣的給個(gè)贊或者關(guān)注~~~