CoreGraphic框架解析 (六)—— 基于CoreGraphic的一個(gè)簡(jiǎn)單繪制示例 (二)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.10.21 星期日

前言

quartz是一個(gè)通用的術(shù)語(yǔ),用于描述在iOSMAC OS X 中整個(gè)媒體層用到的多種技術(shù) 包括圖形赦颇、動(dòng)畫示惊、音頻、適配辞色。Quart 2D 是一組二維繪圖和渲染API骨宠,Core Graphic會(huì)使用到這組APIQuartz Core專指Core Animation用到的動(dòng)畫相關(guān)的庫(kù)相满、API和類层亿。CoreGraphicsUIKit下的主要繪圖系統(tǒng),頻繁的用于繪制自定義視圖雳灵。Core Graphics是高度集成于UIView和其他UIKit部分的棕所。Core Graphics數(shù)據(jù)結(jié)構(gòu)和函數(shù)可以通過(guò)前綴CG來(lái)識(shí)別闸盔。在app中很多時(shí)候繪圖等操作我們要利用CoreGraphic框架悯辙,它能繪制字符串、圖形迎吵、漸變色等等躲撰,是一個(gè)很強(qiáng)大的工具。感興趣的可以看我另外幾篇击费。
1. CoreGraphic框架解析(一)—— 基本概覽
2. CoreGraphic框架解析(二)—— 基本使用
3. CoreGraphic框架解析(三)—— 類波浪線的實(shí)現(xiàn)
4. CoreGraphic框架解析(四)—— 基本架構(gòu)補(bǔ)充
5. CoreGraphic框架解析 (五)—— 基于CoreGraphic的一個(gè)簡(jiǎn)單繪制示例 (一)

開始

在第二部分中拢蛋,您將深入研究Core Graphics,了解繪制漸變和使用CGContexts進(jìn)行變換操作蔫巩。

你現(xiàn)在要離開UIKit的舒適世界谆棱,進(jìn)入Core Graphics的黑社會(huì)。

Apple的這張圖片從概念上描述了相關(guān)的框架:

UIKit是最頂層的圆仔,也是和開發(fā)者最容易接觸的的垃瞧。 您已經(jīng)使用了UIBezierPath,它是Core GraphicsCGPath的UIKit包裝器坪郭。

Core Graphics框架基于Quartz高級(jí)繪圖引擎个从。 它提供低級(jí),輕量級(jí)的2D渲染。 您可以使用此框架來(lái)處理基于路徑的繪圖嗦锐,轉(zhuǎn)換嫌松,顏色管理等等。

關(guān)于下層Core Graphics對(duì)象和函數(shù)的一件事是它們總是有前綴CG奕污,所以它們很容易識(shí)別萎羔。

當(dāng)你到本教程結(jié)束時(shí),你將創(chuàng)建一個(gè)如下所示的圖形視圖:

在繪制圖表視圖之前碳默,您將在故事板中進(jìn)行設(shè)置外驱,并創(chuàng)建動(dòng)畫轉(zhuǎn)換的代碼以顯示圖表視圖。

完整的視圖層次結(jié)構(gòu)如下所示:

前一篇我們已經(jīng)做到了下面這個(gè)程度腻窒。 唯一的區(qū)別是在Main.storyboard中昵宇,CounterView位于另一個(gè)視圖(帶黃色背景)內(nèi)。 構(gòu)建并運(yùn)行儿子,這將是您將看到的:

轉(zhuǎn)到File \ New \ File ...瓦哎,選擇iOS \ Source \ Cocoa Touch Class模板,然后單擊Next柔逼。輸入名稱GraphView作為類名蒋譬,選擇子類UIView并將語(yǔ)言設(shè)置為Swift。單擊Next愉适,然后單擊Create犯助。

現(xiàn)在,在Main.storyboard中维咸,單擊Document Outline中黃色視圖的名稱兩次以重命名剂买,然后將其命名為Container View。將新UIView從對(duì)象庫(kù)拖動(dòng)到Counter View下面的Container View內(nèi)部癌蓖。

Identity Inspector中將新視圖的類更改為GraphView瞬哼。剩下的唯一事情就是為新的GraphView添加約束,類似于在本教程前一部分中的操作:

  • 選中GraphView后租副,按住Control鍵從中心稍微左側(cè)(仍在視圖中)進(jìn)行拖動(dòng)坐慰,然后從彈出菜單中選擇Width
  • 同樣用僧,按住Control鍵在選中GraphView的情況下结胀,從中心稍微向上(仍然在視圖中)進(jìn)行控制 - 拖動(dòng),然后從彈出菜單中選擇Height责循。
  • 按住Control鍵從視圖內(nèi)部向左拖動(dòng)到視圖外部糟港,然后選擇Center Horizontally in Container
  • 按住Control鍵從視圖內(nèi)部向上拖動(dòng)到視圖外部沼死,然后選擇Center Vertically in Container着逐。

Size Inspector中編輯約束常量以匹配以下內(nèi)容:

你的Document Outline應(yīng)該像下面這樣

您需要容器視圖的原因是在Counter ViewGraph View之間進(jìn)行動(dòng)畫過(guò)渡。

轉(zhuǎn)到ViewController.swift并為ContainerGraph Views添加屬性outlets

@IBOutlet weak var containerView: UIView!
@IBOutlet weak var graphView: GraphView!

這為容器視圖和圖形視圖創(chuàng)建了一個(gè)outlet。 現(xiàn)在將它們連接到您在故事板中創(chuàng)建的視圖耸别。

返回Main.storyboard并將Graph ViewContainer View連接到outlet


Seting up the Animated Transition - 設(shè)置動(dòng)畫轉(zhuǎn)場(chǎng)

仍然在Main.storyboard中健芭,將Tap Gesture Recognizer從對(duì)象庫(kù)拖到Document Outline中的Container View

轉(zhuǎn)到ViewController.swift并將此屬性添加到類的頂部:

 
var isGraphViewShowing = false

這只是標(biāo)記當(dāng)前是否顯示圖表視圖。

現(xiàn)在添加tap方法來(lái)進(jìn)行轉(zhuǎn)場(chǎng):

 
@IBAction func counterViewTap(_ gesture: UITapGestureRecognizer?) {
  if (isGraphViewShowing) {
    //hide Graph
    UIView.transition(from: graphView,
                      to: counterView,
                      duration: 1.0,
                      options: [.transitionFlipFromLeft, .showHideTransitionViews],
                      completion:nil)
  } else {
    //show Graph
    UIView.transition(from: counterView,
                      to: graphView,
                      duration: 1.0,
                      options: [.transitionFlipFromRight, .showHideTransitionViews],
                      completion: nil)
  }
  isGraphViewShowing = !isGraphViewShowing
}

UIView.transition(from:to:duration:options:completion :)執(zhí)行水平翻轉(zhuǎn)過(guò)渡秀姐。 其他過(guò)渡是交叉溶解慈迈,垂直翻轉(zhuǎn)和向上或向下卷曲。 轉(zhuǎn)換使用.showHideTransitionViews常量省有,這意味著您不必刪除視圖以防止它在轉(zhuǎn)換中hidden后顯示痒留。

pushButtonPressed(_ :)的末尾添加此代碼:

if isGraphViewShowing {
  counterViewTap(nil)
}

如果用戶在顯示圖表時(shí)按下加號(hào)按鈕,顯示屏將向后擺動(dòng)以顯示計(jì)數(shù)器蠢沿。

最后伸头,要使此轉(zhuǎn)換工作,請(qǐng)返回Main.storyboard并將您的點(diǎn)擊手勢(shì)連接到新添加的counterViewTap(gesture:)方法:

構(gòu)建并運(yùn)行應(yīng)用程序舷蟀。 目前恤磷,您在啟動(dòng)應(yīng)用時(shí)會(huì)看到圖表視圖。 稍后野宜,您將隱藏圖表視圖扫步,因此計(jì)數(shù)器視圖將首先出現(xiàn)。 點(diǎn)按它匈子,您將看到轉(zhuǎn)換翻轉(zhuǎn)河胎。


Analysis of the Graph View - 圖表分析

還記得第1部分中的Painter’s Model嗎? 它解釋了使用Core Graphics繪圖是從圖像背面到前面完成的虎敦,因此在編碼之前需要記住順序游岳。 對(duì)于Flo的圖,那將是:

  • 漸變背景視圖
  • 圖下的剪裁漸變
  • 圖線
  • 圖表的圓圈指向
  • 水平圖線
  • 圖表標(biāo)簽

Drawing a Gradient - 繪制梯度

您現(xiàn)在將在圖表視圖中繪制漸變原茅。

轉(zhuǎn)到GraphView.swift并將代碼替換為:

import UIKit

@IBDesignable class GraphView: UIView {
  
  // 1
  @IBInspectable var startColor: UIColor = .red
  @IBInspectable var endColor: UIColor = .green

    override func draw(_ rect: CGRect) {
      
      // 2
      let context = UIGraphicsGetCurrentContext()!
      let colors = [startColor.cgColor, endColor.cgColor]
      
      // 3
      let colorSpace = CGColorSpaceCreateDeviceRGB()
      
      // 4
      let colorLocations: [CGFloat] = [0.0, 1.0]
      
      // 5
      let gradient = CGGradient(colorsSpace: colorSpace,
                                     colors: colors as CFArray,
                                  locations: colorLocations)!
      
      // 6
      let startPoint = CGPoint.zero
      let endPoint = CGPoint(x: 0, y: bounds.height)
      context.drawLinearGradient(gradient,
                          start: startPoint,
                            end: endPoint,
                        options: [])
    }
}

這里有幾件事要做:

  • 1) 您可以將漸變的開始和結(jié)束顏色設(shè)置為@IBInspectable屬性吭历,以便您可以在故事板中更改它們。
  • 2) CG繪圖函數(shù)需要知道它們將繪制的上下文擂橘,因此您使用UIKit方法UIGraphicsGetCurrentContext()來(lái)獲取當(dāng)前上下文。這是draw(_:)繪制的地方摩骨。
  • 3) 所有上下文都有顏色空間通贞。這可能是CMYK或灰度,但在這里你使用的是RGB色彩空間恼五。
  • 4) 顏色停止描述漸變中的顏色變換的位置昌罩。在這個(gè)例子中,你只有兩種顏色灾馒,紅色變?yōu)榫G色茎用,但你可以有一個(gè)三個(gè)數(shù)的數(shù)組,并且紅色變?yōu)樗{(lán)色變?yōu)榫G色。停止點(diǎn)位于0和1之間轨功,其中0.33是通過(guò)漸變的三分之一旭斥。
  • 5) 創(chuàng)建實(shí)際漸變,定義顏色空間古涧,顏色和顏色停止點(diǎn)垂券。
  • 6) 最后,繪制漸變羡滑。 CGContextDrawLinearGradient()采用以下參數(shù):
    • 要繪制的CGContext
    • CGGradient具有色彩空間菇爪,顏色和停止
    • 起點(diǎn)
    • 終點(diǎn)
    • 用于擴(kuò)展?jié)u變的選項(xiàng)標(biāo)志

漸變將填充整個(gè)draw(_:)rect

設(shè)置Xcode柒昏,以便使用Assistant Editor (Show Assistant Editor…\Counterparts\Main.storyboard)對(duì)您的代碼和故事板進(jìn)行并排查看凳宙,您將看到漸變顯示在圖表視圖上。

在故事板中职祷,選擇Graph View近速。 然后在Attributes Inspector中,將Start Color更改為RGB(250,233,222)堪旧,將End Color更改為RGB(252,79,8)(單擊顏色削葱,然后單擊Other\Color Sliders):

現(xiàn)在做一些清理工作。 在Main.storyboard中淳梦,依次選擇每個(gè)視圖(ViewController主視圖除外)析砸,并將Background Color設(shè)置為Clear Color。 您不再需要黃色爆袍,按鈕視圖也應(yīng)該具有透明背景首繁。

構(gòu)建并運(yùn)行應(yīng)用程序,您會(huì)發(fā)現(xiàn)圖形看起來(lái)更好陨囊,或者至少是它的背景弦疮。


Clipping Areas - 剪裁區(qū)域

剛剛使用漸變時(shí),您填充了整個(gè)視圖的上下文區(qū)域蜘醋。 但是胁塞,您可以創(chuàng)建用作剪切區(qū)域的路徑,而不是用于繪制压语。 剪切區(qū)域允許您定義要填充的區(qū)域啸罢,而不是整個(gè)上下文。

轉(zhuǎn)到GraphView.swift胎食。

首先扰才,在GraphView的頂部添加這些常量,我們稍后將使用它們進(jìn)行繪制:

private struct Constants {
  static let cornerRadiusSize = CGSize(width: 8.0, height: 8.0)
  static let margin: CGFloat = 20.0
  static let topBorder: CGFloat = 60
  static let bottomBorder: CGFloat = 50
  static let colorAlpha: CGFloat = 0.3
  static let circleDiameter: CGFloat = 5.0
}

draw(_:)的頂部添加下面代碼

let path = UIBezierPath(roundedRect: rect,
                  byRoundingCorners: .allCorners,
                        cornerRadii: Constants.cornerRadiusSize)
path.addClip()

這將創(chuàng)建一個(gè)約束漸變的剪切區(qū)域厕怜。 您將很快使用相同的技巧在圖線下繪制第二個(gè)漸變衩匣。

構(gòu)建并運(yùn)行應(yīng)用程序蕾总,看看你的圖表視圖有漂亮的圓角:

注意:使用Core Graphics繪制靜態(tài)視圖通常足夠快,但如果您的視圖移動(dòng)或需要頻繁重繪琅捏,則應(yīng)使用Core Animation層生百。 Core Animation經(jīng)過(guò)優(yōu)化,因此GPU(而不是CPU)可以處理大部分處理午绳。相反置侍,CPU處理Core Graphicsdraw(_ :)中執(zhí)行的視圖繪制。

您可以使用CALayer的cornerRadius屬性創(chuàng)建圓角拦焚,而不是使用剪切路徑蜡坊,但您應(yīng)該針對(duì)您的情況進(jìn)行優(yōu)化。


Tricky Calculations for Graph Points - 圖形點(diǎn)的棘手計(jì)算

現(xiàn)在赎败,您將從繪圖中稍作休息來(lái)制作圖表秕衙。你會(huì)繪制7個(gè)點(diǎn),x軸將是“星期幾”僵刮,y軸將是“喝的杯水的數(shù)量”据忘。

首先,設(shè)置本周的樣本數(shù)據(jù)搞糕。

仍然在GraphView.swift中勇吊,在類的頂部添加以下屬性:

//Weekly sample data
var graphPoints = [4, 2, 6, 4, 5, 8, 3]

這包含代表七天的樣本數(shù)據(jù)。 忽略關(guān)于將其更改為let值的警告窍仰,因?yàn)樯院笪覀冃枰獙⑵渥鳛?code>var汉规。

將此代碼添加到draw(_:)的頂部

let width = rect.width
let height = rect.height

并將此代碼添加到draw(_:)結(jié)束

//calculate the x point
    
let margin = Constants.margin
let graphWidth = width - margin * 2 - 4
let columnXPoint = { (column: Int) -> CGFloat in
  //Calculate the gap between points
  let spacing = graphWidth / CGFloat(self.graphPoints.count - 1)
  return CGFloat(column) * spacing + margin + 2
}

x軸點(diǎn)由7個(gè)等間距點(diǎn)組成。 上面的代碼是一個(gè)閉包表達(dá)式驹吮。 它可以作為函數(shù)添加针史,但對(duì)于像這樣的小型計(jì)算,您可以將它們保持內(nèi)聯(lián)碟狞。

columnXPoint將列作為參數(shù)啄枕,并返回一個(gè)值,其中該點(diǎn)應(yīng)位于x軸上族沃。

添加代碼來(lái)計(jì)算draw(_:)結(jié)束時(shí)的y軸點(diǎn):

// calculate the y point
    
let topBorder = Constants.topBorder
let bottomBorder = Constants.bottomBorder
let graphHeight = height - topBorder - bottomBorder
let maxValue = graphPoints.max()!
let columnYPoint = { (graphPoint: Int) -> CGFloat in
  let y = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight
  return graphHeight + topBorder - y // Flip the graph
}

columnYPoint也是一個(gè)閉包表達(dá)式频祝,它將星期幾數(shù)組中的值作為參數(shù)。 它返回y位置竭业,介于0和最大杯水?dāng)?shù)之間智润。

由于Core Graphics中的原點(diǎn)位于左上角,并且您從左下角的原點(diǎn)繪制圖形未辆,因此columnYPoint會(huì)調(diào)整其返回值,以使圖形朝向您期望的方向锯玛。

繼續(xù)在draw(_:)結(jié)束時(shí)添加線條繪圖代碼

// draw the line graph

UIColor.white.setFill()
UIColor.white.setStroke()
    
// set up the points line
let graphPath = UIBezierPath()

// go to start of line
graphPath.move(to: CGPoint(x: columnXPoint(0), y: columnYPoint(graphPoints[0])))
    
// add points for each item in the graphPoints array
// at the correct (x, y) for the point
for i in 1..<graphPoints.count {
  let nextPoint = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
  graphPath.addLine(to: nextPoint)
}

graphPath.stroke()

在此塊中咐柜,您將為圖形創(chuàng)建路徑兼蜈。 UIBezierPath是從graphPoints中每個(gè)元素的x和y點(diǎn)構(gòu)建的。

故事板中的圖表視圖現(xiàn)在應(yīng)如下所示:

既然您已經(jīng)驗(yàn)證了線條的正確繪制拙友,請(qǐng)從draw(_:)結(jié)束時(shí)刪除它

graphPath.stroke()

這只是為了您可以查看故事板中的行并驗(yàn)證計(jì)算是否正確为狸。


A Gradient Graph - 梯度圖

現(xiàn)在,您將使用路徑作為剪切路徑在此路徑下創(chuàng)建漸變遗契。

首先在draw(_:)結(jié)束時(shí)設(shè)置剪切路徑:

//Create the clipping path for the graph gradient

//1 - save the state of the context (commented out for now)
//context.saveGState()
    
//2 - make a copy of the path
let clippingPath = graphPath.copy() as! UIBezierPath
    
//3 - add lines to the copied path to complete the clip area
clippingPath.addLine(to: CGPoint(x: columnXPoint(graphPoints.count - 1), y:height))
clippingPath.addLine(to: CGPoint(x:columnXPoint(0), y:height))
clippingPath.close()
    
//4 - add the clipping path to the context
clippingPath.addClip()
    
//5 - check clipping path - temporary code
UIColor.green.setFill()
let rectPath = UIBezierPath(rect: rect)
rectPath.fill()
//end temporary code

上述代碼的逐節(jié)細(xì)分:

  • 1) context.saveGState()暫時(shí)被注釋掉了 - 一旦你理解了它的作用辐棒,你馬上就會(huì)回到這一點(diǎn)。
  • 2) 將繪制的路徑復(fù)制到新路徑牍蜂,該路徑定義要用漸變填充的區(qū)域漾根。
  • 3) 完成帶角點(diǎn)的區(qū)域并關(guān)閉路徑。 這會(huì)添加圖表的右下角和左下角鲫竞。
  • 4) 將剪切路徑添加到上下文辐怕。 填充上下文時(shí),實(shí)際只填充剪切的路徑从绘。
  • 5) 填充上下文寄疏。 請(qǐng)記住,rect是傳遞給draw(_ :)的上下文區(qū)域僵井。

故事板中的圖表視圖現(xiàn)在應(yīng)如下所示:

接下來(lái)陕截,您將使用從用于背景漸變的顏色創(chuàng)建的漸變替換可愛的綠色。

draw(_:)結(jié)束中刪除帶有綠色填充的臨時(shí)代碼批什,然后添加以下代碼:

let highestYPoint = columnYPoint(maxValue)
let graphStartPoint = CGPoint(x: margin, y: highestYPoint)
let graphEndPoint = CGPoint(x: margin, y: bounds.height)
        
context.drawLinearGradient(gradient, start: graphStartPoint, end: graphEndPoint, options: [])
//context.restoreGState()

在這個(gè)區(qū)塊中农曲,您會(huì)發(fā)現(xiàn)杯水的最大數(shù)量,并將其作為漸變的起點(diǎn)渊季。

您無(wú)法像使用綠色一樣填充整個(gè)rect朋蔫。 漸變將從上下文的頂部而不是從圖的頂部填充,并且不會(huì)顯示所需的漸變却汉。

記下注釋掉的context.restoreGState() - 在繪制繪圖點(diǎn)的圓圈后驯妄,您將刪除注釋。

draw(_:)結(jié)束時(shí)添加以下內(nèi)容:

//draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()

此代碼繪制原始路徑合砂。

你的圖表現(xiàn)在正在形成:


Drawing the Data Points - 繪制數(shù)據(jù)點(diǎn)

draw(_:)結(jié)束時(shí)青扔,添加以下內(nèi)容:

//Draw the circles on top of the graph stroke
for i in 0..<graphPoints.count {
  var point = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
  point.x -= Constants.circleDiameter / 2
  point.y -= Constants.circleDiameter / 2
      
  let circle = UIBezierPath(ovalIn: CGRect(origin: point, size: CGSize(width: Constants.circleDiameter, height: Constants.circleDiameter)))
  circle.fill()
}

此代碼繪制繪圖點(diǎn)并不是什么新鮮事。 它在計(jì)算的x和y點(diǎn)處為數(shù)組中的每個(gè)元素填充圓形路徑翩伪。

嗯......但是故事板上出現(xiàn)的不是很好的圓形圓點(diǎn)微猖!這是怎么回事?


Context States - 上下文狀態(tài)

圖形上下文可以保存狀態(tài)缘屹。設(shè)置許多上下文屬性(如填充顏色凛剥,變換矩陣,顏色空間或剪輯區(qū)域(fill color, transformation matrix, color space or clip region))時(shí)轻姿,實(shí)際上是為當(dāng)前圖形狀態(tài)設(shè)置它們犁珠。

您可以使用context.saveGState()來(lái)保存狀態(tài)逻炊,它將當(dāng)前圖形狀態(tài)的副本推送到狀態(tài)堆棧。您還可以更改上下文屬性犁享,但是當(dāng)您調(diào)用context.restoreGState()時(shí)余素,原始狀態(tài)將從堆棧中取出,并且上下文屬性將還原炊昆。這就是為什么你看到了你的點(diǎn)的奇怪問(wèn)題桨吊。

仍然在GraphView.swift中,在draw(_ :)中凤巨,取消注釋在創(chuàng)建剪切路徑之前發(fā)生的context.saveGState()视乐,并取消注釋在使用剪切路徑之后發(fā)生的context.restoreGState()

通過(guò)這樣做磅甩,你:

  • 1) 使用context.saveGState()將原始圖形狀態(tài)推送到堆棧炊林。
  • 2) 將剪切路徑添加到新的圖形狀態(tài)。
  • 3) 在剪切路徑中繪制漸變卷要。
  • 4) 使用context.restoreGState()恢復(fù)原始圖形狀態(tài)渣聚。這是您添加剪切路徑之前的狀態(tài)。

你的圖形線和圓圈現(xiàn)在應(yīng)該更加清晰:

draw(_:)結(jié)束時(shí)僧叉,添加代碼以繪制三條水平線:

//Draw horizontal graph lines on the top of everything
let linePath = UIBezierPath()

//top line
linePath.move(to: CGPoint(x: margin, y: topBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: topBorder))

//center line
linePath.move(to: CGPoint(x: margin, y: graphHeight/2 + topBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: graphHeight/2 + topBorder))

//bottom line
linePath.move(to: CGPoint(x: margin, y:height - bottomBorder))
linePath.addLine(to: CGPoint(x:  width - margin, y: height - bottomBorder))
let color = UIColor(white: 1.0, alpha: Constants.colorAlpha)
color.setStroke()
    
linePath.lineWidth = 1.0
linePath.stroke()

此代碼中沒有任何內(nèi)容是新的奕枝。 你所做的只是移動(dòng)到一個(gè)點(diǎn)并繪制一條水平線。


Adding the Graph Labels - 添加圖形標(biāo)簽

現(xiàn)在瓶堕,您將添加標(biāo)簽以使圖形用戶友好隘道。

轉(zhuǎn)到ViewController.swift并添加這些outlet屬性:

//Label outlets
@IBOutlet weak var averageWaterDrunk: UILabel!
@IBOutlet weak var maxLabel: UILabel!
@IBOutlet weak var stackView: UIStackView!

這會(huì)為您想要?jiǎng)討B(tài)更改文本的兩個(gè)標(biāo)簽(平均水量標(biāo)簽,最大水量標(biāo)簽)以及帶有日期名稱標(biāo)簽的StackView添加outlets郎笆。

現(xiàn)在轉(zhuǎn)到Main.storyboard并將以下視圖添加為圖表視圖的子視圖:

  • 1) UILabel文字“Water Drunk”
  • 2) UILabel谭梗,文字Average
  • 3) UILabel,文本“2”宛蚓,旁邊是Average標(biāo)簽
  • 4) UILabel激捏,文本“99”,右側(cè)對(duì)齊圖形頂部
  • 5) UILabel凄吏,文本“0”远舅,右對(duì)齊到圖形的底部
  • 6) 一個(gè)水平StackView,每周的每一天都有標(biāo)簽 - 每個(gè)文本的代碼都會(huì)更改痕钢。 中心對(duì)齊图柏。

按住Shift鍵選擇所有標(biāo)簽,然后將字體更改為Avenir Next Condensed, Medium style任连。

averageWaterDrunk蚤吹,maxLabelstackView連接到Main.storyboard中的相應(yīng)視圖。 按住Control鍵從View Controller拖動(dòng)到正確的標(biāo)簽随抠,然后從彈出窗口中選擇outlet

現(xiàn)在您已完成圖形視圖的設(shè)置距辆,在Main.storyboard中選擇Graph View并選中Hidden余佃,以便在應(yīng)用程序首次運(yùn)行時(shí)不顯示圖形暮刃。

轉(zhuǎn)到ViewController.swift并添加此方法以設(shè)置標(biāo)簽:

 
func setupGraphDisplay() {

  let maxDayIndex = stackView.arrangedSubviews.count - 1
  
  //  1 - replace last day with today's actual data
  graphView.graphPoints[graphView.graphPoints.count - 1] = counterView.counter
  //2 - indicate that the graph needs to be redrawn
  graphView.setNeedsDisplay()
  maxLabel.text = "\(graphView.graphPoints.max()!)"
    
  //  3 - calculate average from graphPoints
  let average = graphView.graphPoints.reduce(0, +) / graphView.graphPoints.count
  averageWaterDrunk.text = "\(average)"
    
  // 4 - setup date formatter and calendar
  let today = Date()
  let calendar = Calendar.current
    
  let formatter = DateFormatter()
  formatter.setLocalizedDateFormatFromTemplate("EEEEE")
  
  // 5 - set up the day name labels with correct days
  for i in 0...maxDayIndex {
    if let date = calendar.date(byAdding: .day, value: -i, to: today),
      let label = stackView.arrangedSubviews[maxDayIndex - i] as? UILabel {
      label.text = formatter.string(from: date)
    }
  }
}

這看起來(lái)有點(diǎn)繁瑣跨算,但需要設(shè)置日歷并檢索一周中的當(dāng)前日期:

  • 1) 您將今天的數(shù)據(jù)設(shè)置為圖形數(shù)據(jù)數(shù)組中的最后一項(xiàng)。在最終項(xiàng)目中椭懊,您將通過(guò)將其替換為60天的樣本數(shù)據(jù)來(lái)擴(kuò)展它诸蚕,并且您將包含一個(gè)方法,該方法可以分割出最后x天的數(shù)據(jù)氧猬。數(shù)組背犯,但這超出了本次會(huì)議的范圍。
  • 2) 如果今天的數(shù)據(jù)有任何變化盅抚,請(qǐng)重新繪制圖表漠魏。
  • 3) 在這里你使用Swift的reduce來(lái)計(jì)算本周喝的杯水量,這是一個(gè)非常有用的方法來(lái)總結(jié)數(shù)組中的所有元素妄均。
  • 4) 此部分以一種方式設(shè)置DateFormatter柱锹,它將獲得一天名稱的第一個(gè)字母。
  • 5) 這個(gè)循環(huán)遍歷stackView中的所有標(biāo)簽丰包,我們?yōu)槿掌诟袷交绦蛑械拿總€(gè)標(biāo)簽設(shè)置文本禁熏。

仍然在ViewController.swift中,從counterViewTap(_ :)調(diào)用這個(gè)新方法邑彪。在條件的else部分瞧毙,注釋顯示show graph,添加以下代碼:

setupGraphDisplay()

運(yùn)行該應(yīng)用程序寄症,然后單擊計(jì)數(shù)器宙彪,查看效果:


Mastering the Matrix - 掌握矩陣

你的應(yīng)用看起來(lái)非常好! 您在第一部分中創(chuàng)建的計(jì)數(shù)器視圖可以進(jìn)行改進(jìn)有巧,例如添加標(biāo)記以指示每個(gè)要喝的杯水:

現(xiàn)在您已經(jīng)對(duì)CG函數(shù)進(jìn)行了一些實(shí)踐释漆,您將使用它們來(lái)旋轉(zhuǎn)和轉(zhuǎn)換繪圖上下文。

請(qǐng)注意剪决,這些標(biāo)記從中心輻射:

除了繪制上下文之外灵汪,您還可以選擇通過(guò)旋轉(zhuǎn),縮放和轉(zhuǎn)換上下文的變換矩陣來(lái)操縱上下文柑潦。

起初享言,這看起來(lái)很令人困惑,但在你完成這些練習(xí)后渗鬼,它會(huì)更有意義览露。 變換的順序很重要,因此首先我將概述您將使用圖表做什么譬胎。

下圖是旋轉(zhuǎn)上下文然后在上下文中心繪制一個(gè)矩形的結(jié)果差牛。

在旋轉(zhuǎn)上下文之前繪制黑色矩形命锄,然后是綠色矩形,然后是紅色矩形偏化。 有兩點(diǎn)需要注意:

  • 1) 上下文在左上角旋轉(zhuǎn)(0,0)
  • 2) 矩形仍在上下文的中心繪制脐恩,但在上下文旋轉(zhuǎn)后。

當(dāng)您繪制計(jì)數(shù)器視圖的標(biāo)記時(shí)侦讨,您將首先變換上下文驶冒,然后旋轉(zhuǎn)它。

在此圖中韵卤,矩形標(biāo)記位于上下文的最左上角骗污。 藍(lán)線勾勒出變換的上下文,然后上下文旋轉(zhuǎn)(紅色虛線)并再次變換沈条。

當(dāng)紅色矩形標(biāo)記最終被繪制到上下文中時(shí)需忿,它將以一定角度出現(xiàn)在視圖中。

旋轉(zhuǎn)上下文并平移以繪制紅色標(biāo)記后蜡歹,需要將其重置為中心屋厘,以便可以旋轉(zhuǎn)上下文并再次平移以繪制綠色標(biāo)記。

就像在Graph View中使用剪切路徑保存上下文狀態(tài)一樣季稳,每次繪制標(biāo)記時(shí)擅这,都將使用變換矩陣保存和恢復(fù)狀態(tài)。

轉(zhuǎn)到CounterView.swift并將此代碼添加到draw(_:)結(jié)束以將標(biāo)記添加到計(jì)數(shù)器:

//Counter View markers
let context = UIGraphicsGetCurrentContext()!
  
//1 - save original state
context.saveGState()
outlineColor.setFill()
    
let markerWidth: CGFloat = 5.0
let markerSize: CGFloat = 10.0

//2 - the marker rectangle positioned at the top left
let markerPath = UIBezierPath(rect: CGRect(x: -markerWidth / 2, y: 0, width: markerWidth, height: markerSize))

//3 - move top left of context to the previous center position  
context.translateBy(x: rect.width / 2, y: rect.height / 2)
    
for i in 1...Constants.numberOfGlasses {
  //4 - save the centred context
  context.saveGState()
  //5 - calculate the rotation angle
  let angle = arcLengthPerGlass * CGFloat(i) + startAngle - .pi / 2
  //rotate and translate
  context.rotate(by: angle)
  context.translateBy(x: 0, y: rect.height / 2 - markerSize)
   
  //6 - fill the marker rectangle
  markerPath.fill()
  //7 - restore the centred context for the next rotate
  context.restoreGState()
}

//8 - restore the original state in case of more painting
context.restoreGState()

這就是你剛才所做的:

  • 1) 在操作上下文的矩陣之前景鼠,您可以保存矩陣的原始狀態(tài)仲翎。
  • 2) 定義路徑的位置和形狀 - 但您還沒有繪制它。
  • 3) 移動(dòng)上下文铛漓,以便在上下文的原始中心周圍進(jìn)行旋轉(zhuǎn)溯香。 (上圖中的藍(lán)線。)
  • 4) 對(duì)于每個(gè)標(biāo)記浓恶,首先保存居中的上下文狀態(tài)玫坛。
  • 5) 使用先前計(jì)算的單個(gè)角度,確定每個(gè)標(biāo)記的角度并旋轉(zhuǎn)和轉(zhuǎn)換上下文包晰。
  • 6) 在旋轉(zhuǎn)和變換的上下文的左上角繪制標(biāo)記矩形湿镀。
  • 7) 恢復(fù)居中上下文的狀態(tài)。
  • 8) 恢復(fù)沒有旋轉(zhuǎn)或變換的上下文的原始狀態(tài)伐憾。

現(xiàn)在構(gòu)建并運(yùn)行應(yīng)用程序勉痴,并欣賞Flo的美麗且信息豐富的UI:

后記

本篇主要講述了基于CoreGraphic的一個(gè)簡(jiǎn)單繪制示例,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末树肃,一起剝皮案震驚了整個(gè)濱河市蒸矛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖雏掠,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斩祭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡乡话,警方通過(guò)查閱死者的電腦和手機(jī)摧玫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蚊伞,“玉大人席赂,你說(shuō)我怎么就攤上這事∈逼龋” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵谓晌,是天一觀的道長(zhǎng)掠拳。 經(jīng)常有香客問(wèn)我,道長(zhǎng)纸肉,這世上最難降的妖魔是什么溺欧? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮柏肪,結(jié)果婚禮上姐刁,老公的妹妹穿的比我還像新娘。我一直安慰自己烦味,他們只是感情好聂使,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谬俄,像睡著了一般柏靶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溃论,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天屎蜓,我揣著相機(jī)與錄音,去河邊找鬼钥勋。 笑死炬转,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的算灸。 我是一名探鬼主播扼劈,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乎婿!你這毒婦竟也來(lái)了测僵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捍靠,沒想到半個(gè)月后沐旨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡榨婆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年磁携,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片良风。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谊迄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出烟央,到底是詐尸還是另有隱情统诺,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布疑俭,位于F島的核電站粮呢,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钞艇。R本人自食惡果不足惜啄寡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哩照。 院中可真熱鬧挺物,春花似錦、人聲如沸飘弧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)眯牧。三九已至蹋岩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間学少,已是汗流浹背剪个。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留版确,地道東北人扣囊。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像绒疗,于是被迫代替她去往敵國(guó)和親侵歇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容