原文:https://code.tutsplus.com/zh-...
原做:Akiel Khan
翻譯:Susnm 程序員
自從蘋果跟swift一塊兒推出了Xcode6以來,到如今的Xcode7.3.1,playground有了一個長足的發展。 隨之而來的是,新功能和更好的穩定性,從而有可能進化成一個快速佈局和結合概念的工具。編程
做爲一個開發者,總有那麼一瞬間,對某一個app有了靈感,你想去快速用代碼建立佈局來展現你idea的本質。 或者你想去驗證你對UIKit代碼片斷的理解及行爲。 若是你像我同樣,寧肯避免麻煩,建立Xcode項目,而不是處理無數個因素,好比設備類型,資源和設置等。 而這些決定都可以被推遲一直到你建立起你的構思肯定核心代碼以後再作決定。swift
在這個教程中,我建立了一個以卡片爲基礎的記憶累遊戲,而這一切全都是在playground完成的。 這是一個大衆的,衆所周知的遊戲,因此這裏沒有首創性。 遊戲由8對相同的卡片組成(因此一共16張卡片)正面朝下放在4*4的網格中。api
玩家須要翻轉快速的顯示兩張卡片,而後會快速的翻轉回去。 遊戲的目的是讓玩家嘗試記住牌的位置,發現相同的一對,會被從遊戲中刪掉。 遊戲在網格全部卡片被清除以後結束。數組
遊戲以觸摸爲基礎融合簡單的視圖動畫。 你能學習到如何修改你的遊戲,並生動的看到你修改的變化。xcode
打開Xcode,而後從Xcode的File menu中選擇New > Playground...。 給playground取一個名字,好比MemoryGameXCPTut,將Platform選項設置爲iOS,保存playground。 在這個教程中我使用的是Xcode 7.3.1。網絡
找到你本身使用playground的方式
讓咱們花一些時間來熟悉咱們的playground界面。 若是你已經熟悉了playgrounds,你能夠跳過這個章節。閉包
一個playground能夠有多個pages,每個都有本身的live view和本身的sources/resources文件夾。 在這個教程中咱們不須要使用多個pages。 Playgrounds支持markup格式,容許你添加富文本到playground,並在多個playground的pages之間連接。app
建立一個playground以後你看到的第一件東西就是playground的代碼編輯區域。 這是你寫代碼的地方,你修改的效果會在live view上顯示。 你可使用Command-0快捷鍵來隱藏或顯示項目導航欄。 在項目導航欄,你能夠看到兩個文件夾,Source和Resources。框架
在Source文件夾中,你能添加一些輔助代碼到一個或多個swift文件中,好比自定義類,視圖控制器和視圖。 即便你在這裏定義了大量的佈局邏輯代碼,當你的app動起來的時候,它也是在藏在背後輔助的。
將輔助代碼放在Sources文件夾下的一個優點是這樣它能在你每次編輯和保存文件後自動編譯。 經過這個方法,你在playground裏作修改時,能更快速的在live view中獲得反饋。 回到playground,你可以訪問在輔助代碼中你暴露出來的以public修飾的屬性和方法,從而來影響你app的行爲。
你能添加額外的資源到Resources文件夾中,好比照片。
在這個教程中,你須要頻繁的在咱們建立在Sources文件夾中的swift文件和playground文件(技術上它是swift文件,可是咱們不將使用文件名來引用它)之間切換。 在教程中咱們也使用Assistant Editor,讓它顯示Timeline,在playground的代碼旁邊查看live view。 你在playground中作的任何改變都會馬上(好吧是幾秒鐘以內)反應到live ouptput中。 你也能跟live view觸摸反饋,它是UI對象。 爲了確保你能作這些,看一眼下面的插圖。
對應綠色的數字,我給出了下面的註解:
這個按鈕是爲了只顯示主編輯區域,用來隱藏Assistant Editor。
這個按鈕用來顯示Assistant Editor。 Assistant Editor顯示在主編輯區域的右邊。 這個編譯器會幫助咱們顯示相關的文件,好比主編輯器的文件的對應副本。
從左到右,有兩個按鈕各自用來切換項目導航欄和調試區域的顯示。 在控制器咱們能夠輸出一些東西的狀態來檢測。
主編譯器上面的jump bar是用來導航到特殊的文件。 點擊項目的名字兩次帶你回到playground。 或者你也可使用項目導航欄回去playground。
有時候,當看playground的時候,你須要去確保Assistant Editor是顯示的Timeline,而不是一些其餘的文件。 下面的插圖教你怎麼作。 在Assistant Editor,選擇Timeline,playground的副本,而不是Manual,它容許你在Assistant Editor中顯示任意的文件。
當你從Sources文件夾中編輯一個文件時,做爲它的副本,Assistant Editor顯示你代碼的界面,它顯示的是定義和佈局沒有實際的實現。 我在Source文件夾中修改一個文件時,更喜歡隱藏Assistant Editor,僅僅在playground中顯示Assistant Editor用來看動視圖。
去實現playground的特殊的能力,你須要導入 XCPlayground這個module。
import XCPlayground
你要給XCPlayground
對象的currentPage
的liveView
屬性設置一個遵循XCPlaygroundLiveViewable
協議的對象。 它能夠是一個自定義的類或者是一個UIView
或UIViewController
實例。
我在這個教程中添加了一些須要用到的images。 下載images,解壓後添加這些Images到項目導航中的Resources文件夾下的Images文件夾。
確保只是拖images到Resources文件夾下,而不是Resources/Images文件夾下。
刪除playground中的代碼。 右鍵點擊Sources文件夾,從菜單欄中先擇New File。 設置文件的名字爲Game.swift。
添加下面的代碼到Game.swift。 確保在每次添加了代碼後你保存了。
import UIKit import XCPlayground import GameplayKit // (1) public extension UIImage { // (2) public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let cgImage = image.CGImage else { return nil } self.init(CGImage: cgImage) } } let cardWidth = CGFloat(120) // (3) let cardHeight = CGFloat(141) public class Card: UIImageView { // (4) public let x: Int public let y: Int public init(image: UIImage?, x: Int, y: Int) { self.x = x self.y = y super.init(image: image) self.backgroundColor = .grayColor() self.layer.cornerRadius = 10.0 self.userInteractionEnabled = true } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
我添加了一些數字標註用來解釋一些節點實現的:
除了UIKit
和XCPlayground
,咱們也添加了GamePlayKit
。 這個框架包括了一個便捷的方法來幫助咱們執行一個隨機打亂一個數組元素的方法。
在UIKit
方法的幫助下,經過這個UIImage
的擴展,咱們能夠快速的gray顏色的任意大小的images。 咱們將使用這個做爲playing card的背景圖片。
常量cardHeight
和cartWidth
表示card image的尺寸,而後以這個尺寸爲基礎計算其餘的尺寸。
Card
類,繼承自UIImageView
,表示一張卡片。 即便咱們設置了Card
類的一些屬性,咱們建立這個類的目的是幫助咱們在遊戲中識別遊戲卡片的子視圖。 卡片也有它們的屬性x
和y
來記住它們在grid中的位置。
添加下列代碼到Game.swift,就在以前代碼的後面:
public class GameController: UIViewController { // (1): public variables so we can manipulate them in the playground public var padding = CGFloat(20)/* { didSet { resetGrid() } } */ public var backImage: UIImage = UIImage( color: .redColor(), size: CGSize(width: cardWidth, height: cardHeight))! // (2): computed properties var viewWidth: CGFloat { get { return 4 * cardWidth + 5 * padding } } var viewHeight: CGFloat { get { return 4 * cardHeight + 5 * padding } } var shuffledNumbers = [Int]() // stores shuffled card numbers // var firstCard: Card? // uncomment later public init() { super.init(nibName: nil, bundle: nil) preferredContentSize = CGSize(width: viewWidth, height: viewHeight) shuffle() setupGrid() // uncomment later: // let tap = UITapGestureRecognizer(target: self, action: #selector(GameController.handleTap(_:))) // view.addGestureRecognizer(tap) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func loadView() { view = UIView() view.backgroundColor = .blueColor() view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) } // (3): Using GameplayKit API to generate a shuffling of the array [1, 1, 2, 2, ..., 8, 8] func shuffle() { let numbers = (1...8).flatMap{[$0, $0]} shuffledNumbers = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(numbers) as! [Int] } // (4): Convert from card position on grid to index in the shuffled card numbers array func cardNumberAt(x: Int, _ y: Int) -> Int { assert(0 <= x && x < 4 && 0 <= y && y < 4) return shuffledNumbers[4 * x + y] } // (5): Position of card's center in superview func centerOfCardAt(x: Int, _ y: Int) -> CGPoint { assert(0 <= x && x < 4 && 0 <= y && y < 4) let (w, h) = (cardWidth + padding, cardHeight + padding) return CGPoint( x: CGFloat(x) * w + w/2 + padding/2, y: CGFloat(y) * h + h/2 + padding/2) } // (6): setup the subviews func setupGrid() { for i in 0..<4 { for j in 0..<4 { let n = cardNumberAt(i, j) let card = Card(image: UIImage(named: String(n)), x: i, y: j) card.tag = n card.center = centerOfCardAt(i, j) view.addSubview(card) } } } // (7): reset grid /* func resetGrid() { view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) for v in view.subviews { if let card = v as? Card { card.center = centerOfCardAt(card.x, card.y) } } } */ override public func viewDidAppear(animated: Bool) { for v in view.subviews { if let card = v as? Card { // (8): failable casting UIView.transitionWithView( card, duration: 1.0, options: .TransitionFlipFromLeft, animations: { card.image = self.backImage }, completion: nil) } } } }
兩個屬性,padding
和backImage
,是被定義爲public
,爲了以後咱們能在playground中訪問到。 它們在gride上顯示空白的card和各自的card背後顯示image。 注意兩個屬性都已經給了初始值,一個20的padding間距值和紅色的card image邊框。 你如今線忽略那些代碼上的註釋了。
咱們經過計算屬性肯定寬度和高度。 理解viewWidth
計算,記住,每一行有四張卡片,咱們須要去給每張卡片之間設置間距。 相同的思路計算viewHeight
。
代碼(1...8).flatmap{[$0, $0]}
是一個便捷的方法產生數組[1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8]
。 若是你不熟悉函數式編程,你能夠寫for
循環生成數組。 使用框架GamePlayKit
的方法,咱們能夠洗牌數組中的數字。 這些數字對應八對卡片。 每個數字對於卡片圖片的名字(舉個例子,shuffledArray
中的值1
對應1.png)。
咱們寫了一個方法,將4x4網格上的卡片位置轉化爲shuffledNumbers
數組中的下標。 因素4
反應了算數計算中每排的4張卡片。
咱們也有一個方法以卡片的尺寸和間隔計算出卡片的位置(center
屬性)。
setupGrid()
方法在控制器的初始化方法中被調用。 這是4x4的Card
網格佈局。 以shuffledNumbers
數組爲基礎給每個卡片分配了惟一標示,儲存這個值給tag
屬性,它是繼承子card的基礎類UIView
。 遊戲的原理是,咱們經過比較兩個卡片的tag
判斷是否匹配。 雖然這個造型方案很是簡陋,可是對於面前的須要足夠了。
這段註銷的代碼片斷能在padding改變的時候從新定位cards。 記住咱們定義padding
爲public,由於只有這樣咱們纔可以在playground中訪問。
代碼viewDidAppear(_:)
會在控制器的view變成可見以後當即執行。 咱們遍歷view的subViews,若是subview是Card
類的實例(經過as?
可失敗操做符),在if
的內部定義過分的執行。 在這裏咱們將改變image在cards中的顯示,從每張卡片定義的漫畫圖片轉換爲普通的backImage
。 這個過分伴隨着從左向右翻轉的動畫,給cards以物理的翻轉表現。 若是你不熟悉UIView
是怎麼工做的,這啃呢個看上去有一點奇怪。 即便咱們單獨的在循環中給card提阿佳動畫,這些動畫是同時過分喝執行的,這就是咱們須要的卡片反轉效果。
從新打開playground,用下面的代碼代替編輯器中的全部的text:
import XCPlayground import UIKit let gc = GameController() XCPlaygroundPage.currentPage.liveView = gc
確保timeline是可見的。 控制器的view會顯示4x4的卡片網格,且會旋轉給咱們看卡片背後的圖片。 如今,我不能對view作更多的事,由於咱們沒有實現任何跟交互有關的代碼。 如今咱們將開始定義了。
如今,讓咱們改變卡片背面,從紅色的方塊改成圖片(就是Resources文件夾下的 b.png)。 添加下面的代碼到playground的底部。
gc.backImage = UIImage(named: "b")!
一兩秒之後,咱們能夠看到卡片的編碼從紅色改成了手指的圖片。
如今,咱們嘗試修改padding
屬性,它在Game.swift中分配了一個默認的值20。 做爲結果,卡片之間的間距應該增長。 添加下面的代碼到playground的底部:
gc.padding = 75
等到動視圖刷新,看到。。。沒有事情發生變化。
在繼續以前,咱們須要記住實體,好比控制器,它們分配了views,有一個複雜的生命週期。 咱們將更關注後者,就是views。 建立和更新一個控制器的view是一個多階段的過程。 在view的生命週期的特殊位置,通知是UIVIewController
的事件,通知它,什麼發生了。 更重要的,程序員可以經過插入代碼去直接干預這個通知的進程。
loadView()
和viewDidAppear(_:)
兩個方法咱們用來干預view的生命週期。 這些觀點超出了咱們的討論範圍,可是問題的關鍵是,代碼在playground中,playground的liveView
做爲控制器的任務是執行viewWillAppear(_:)
和viewDidAppear(_:)
。 你能在playground中修改一些屬性來驗證,或者在兩個方法中添加print語句來顯示這些屬性的值。
padding
的值改變沒有預料到的視圖效果,這個時候,view和subviews已經佈局好了。 記住,不管何時你改變代碼,playground都會從頭開始運行。 這種狀況不僅是特別的對playground。 即便你開放在模擬器上或者物理設備中運行,一般你也要屢次添加額外代碼來確保屬性值的改變確實對view的表現和內容有效。
你可能問爲何咱們要改變backImage
屬性,可是看到的結果並無任何特別的。 顯然backImage
屬性是第一次viewDidAppear(_:)
的時候使用的,來肯定新值。
咱們處理這種狀況的方法是去監聽padding
屬性的改變,重置view和subViews。 幸運的是,這個使用swift中的屬性監視器功能是很方便的。 打開Game.swift中關閉的代碼段resetGrid()
方法。
// (7): reset grid func resetGrid() { view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight) for v in view.subviews { if let card = v as? Card { card.center = centerOfCardAt(card.x, card.y) } } }
這個方法經過新值viewWidth
和viewHeight
來計算每一個Card
對象和views的frame的位置。 使用剛修改的padding
值爲基礎從新計算那些屬性計算。
使用didSet
監聽器來修改padding
的代碼,這個名字說明,不管你何時給padding
設置值都會被喚起這個監聽器:
// (1): public variables so we can manipulate them in the playground public var padding = CGFloat(20) { didSet { resetGrid() } }
resetGrid()
方法會從新刷新來回應這個新間距。 你能夠在playground中驗證。
這裏看上去咱們很容易的修復了這個事情。 實際中,當咱們第一次以爲想去跟padding
屬性交互的時候,咱們必須回去,改變Game.swift
中的代碼。 舉個例子,我不得不去從Card
中剝離出單獨的center計算方法centerOfCardAt(_:_:)
,以便不管你何時,都能簡便獨立的從新計算卡片的位置來佈局。
把viewWidth
和viewHeight
做爲計算屬性也是有幫助的。 當重寫一些代碼的時候,你應該明白儘可能在設計以前有過權衡,經過一些事先思考和經驗,會減小代碼的重寫。
如今是時候去執行遊戲的邏輯,打開觸摸交互。 取消GameController類的中定義的屬性firstCard
的註釋:
var firstCard: Card?
遊戲的原理是須要用到兩個卡片,一個接一個翻開。 這個firstCard屬性用來保持跟蹤玩家執行翻牌的第一張翻牌或者沒有。
添加下面的方法到GameController
類的最底部,在最終的花括號以前:
func handleTap(gr: UITapGestureRecognizer) { let v = view.hitTest(gr.locationInView(view), withEvent: nil)! if let card = v as? Card { UIView.transitionWithView( card, duration: 0.5, options: .TransitionFlipFromLeft, animations: {card.image = UIImage(named: String(card.tag))}) { // trailing completion handler: _ in card.userInteractionEnabled = false if let pCard = self.firstCard { if pCard.tag == card.tag { UIView.animateWithDuration( 0.5, animations: {card.alpha = 0.0}, completion: {_ in card.removeFromSuperview()}) UIView.animateWithDuration( 0.5, animations: {pCard.alpha = 0.0}, completion: {_ in pCard.removeFromSuperview()}) } else { UIView.transitionWithView( card, duration: 0.5, options: .TransitionFlipFromLeft, animations: {card.image = self.backImage}) { _ in card.userInteractionEnabled = true } UIView.transitionWithView( pCard, duration: 0.5, options: .TransitionFlipFromLeft, animations: {pCard.image = self.backImage}) { _ in pCard.userInteractionEnabled = true } } self.firstCard = nil } else { self.firstCard = card } } } }
這是一個很長的方法。 這事由於要獲取全部必要的觸摸事件,遊戲的邏輯聯繫動畫在一個方法中。 讓咱們看看這個方法是怎麼工做的:
首先,有一個驗證來確保時間上觸摸的是Card
實例。 這個as?
跟咱們以前使用的同樣。
若是玩家觸摸的是Card
實例,咱們使用以前執行相似的動畫翻轉卡片。 僅僅新的一部分是咱們使用了閉包來處理,他會在動畫執行完畢後被調用,而後使用card的屬性userInteractionEnabled
臨時關閉card的交互。 這是防止玩家翻轉相同的卡片。 注意_ in
結構在方法中被使用了屢次。 這只是說咱們忽略完成閉包捕獲的參數Bool
。
咱們以firstCard
是否被分配了nil的可選值綁定(swift的相似if let
的結構)爲基礎來執行代碼。
若是firstCard
不是nil,那麼這是玩家單獨翻的第二張卡片。 咱們如今須要比較卡片的牌面和前面的一個是否匹配(經過比較值tag
)。 若是相同,咱們使用動畫讓卡片消失(經過設置卡片的alpha
爲0)。 咱們也會從view上移除那些卡片。 若是tag是不想等的,意思就是卡片沒有匹配,咱們簡單的將他們翻轉回去,設置它們的userInteractionEnabled
爲true
,爲了玩家能夠再次選中他們。
根據當前的firstCard
的值,咱們設置這個爲nil
,或者顯示card。 這就是咱們兩個成功觸摸的代碼邏輯。
最後,取消對下面兩行在GameCotroller
的初始化中的註釋,添加一個tap手勢給view。 當tap手勢發現一個tap的時候,方法handleTap()
會被調用:
let tap = UITapGestureRecognizer(target: self, action: #selector(GameController.handleTap(_:))) view.addGestureRecognizer(tap)
回到playground的timeline玩一下這個記憶遊戲。 比以前分配的padding
減小了很多感受好多了。
方法handleTap(_:)
裏的代碼是我第一次寫的時候的版本。 一個反對的想法產生了,做爲一個單個方法,它太長了。 或者說這個代碼不足夠面對對象,卡片的翻轉邏輯和動畫應該分離到Card
類中。 當那些想法產生的時候,記住,快速佈局是咱們這個教程的目的。
一旦咱們作了一些工做,我決定未來想要去追逐想法,咱們無疑將須要考慮代碼的重構。 換句話說,首先要讓它工做,而後纔是讓它快速的、優雅的、完美的。。。
當如今教程的主要部分結束了,有趣的一部分是,我想去給你顯示我會直接在playground中寫觸摸事件的代碼。 我首先在GameController
中添加一個方法,容許咱們快速的撇一眼卡片的牌。 添加下面的代碼到gameController
類,就在方法handleTap(_:)
的下面:
public func quickPeek() { for v in view.subviews { if let card = v as? Card { card.userInteractionEnabled = false UIView.transitionWithView(card, duration: 1.0, options: .TransitionFlipFromLeft, animations: {card.image = UIImage(named: String(card.tag))}) { _ in UIView.transitionWithView(card, duration: 1.0, options: .TransitionFlipFromLeft, animations: {card.image = self.backImage}) { _ in card.userInteractionEnabled = true } } } } }
咱們想要在playground內實現註銷和激活這個「quick peek」的能力的功能。 一個方法就是去建立一個public的Bool
在GameController
的類中,因此能在playground中設置。 當人,咱們將不得不在GameController
中寫一個手勢控制器,經過不一樣的手勢激活,將喚醒quickPeek()
。
另外一個方法是直接在playground中寫手勢控制器的代碼。 這樣作的優勢是咱們可以自定義的合併一些額外的代碼調用quickPeek()
。 這就是我接下來要作的。 添加下面的代碼到playground的下面:
class LPGR { static var counter = 0 @objc static func longPressed(lp: UILongPressGestureRecognizer) { if lp.state == .Began { gc.quickPeek() counter += 1 print("You peeked \(counter) time(s).") } } } let longPress = UILongPressGestureRecognizer(target: LPGR.self, action: #selector(LPGR.longPressed)) longPress.minimumPressDuration = 2.0 gc.view.addGestureRecognizer(longPress)
爲了激活quick peek的功能,我將使用一個長按的手勢,玩家在屏幕上按住它們一下子。 咱們使用兩秒做爲觸發條件。
爲了控制這個手勢,我建立了一個類,LPRG
(長按手勢識別器的縮寫),還有一個static
變量屬性,counter
,用來記錄咱們看了多少次,和一個static
的方法longPressed(_:)
來控制手勢。
經過使用static
修飾符,咱們能夠避免建立LPGR
的實例,由於被static修飾的實體是LPGR
類型的class,而不是特殊的實例。
除此以外,該方法沒有特別的遊戲。 反而又一個複雜的理由,咱們須要去用關鍵詞@objc
修飾方法來讓編譯器不報錯。 注意,如今使用LPGR.self
來指引對象類型。 還要注意,在這個手勢控制器中,咱們要檢查手勢的state
是.Begin
。 這是由於長按手勢是過程的,只要玩家保持它們的手指在屏幕上手勢識別器就會一直執行。 咱們但願每一個手指按壓代碼執行一次,當手勢第一次被識別的時候我嘛執行代碼。
counter是代碼自定義增長的,不是做爲功能被GameController
類所提供的。 你能夠在最下面的控制檯看print(_:)
方法(在peek幾回以後)的輸出。
希望這個教程示範了一個有趣的在Xcode的playground中快速交互佈局的例子。 使用playground除了我以前提到的理由外,你能夠用來構成其餘那些有用的狀況。 舉個例子:
使用示範佈局功能給你的委託人看,讓他們有所選擇,製造更加積極的自定義反饋,而不須要追究代碼的詳細內容。
模擬開發,好比你的物理,學生能夠玩一些屬性值,從而觀察模擬器是什麼反應。 事實上,蘋果公司放出了一個使人印象深入的playground,展現了它們的互動和UIDynamics
API的物理引擎。 我強烈推薦你去看一下這個。
當在使用playground的時候,以示範操做和教授爲目的,你將可能經過使用playground的markup做爲富文本和導航的能力而更佳開闊。
Xcode團隊提交的改善的playground做爲IDE的新版本放出了。 最大的消息就是Xcode8,目前是beta版本,將有一個新功能playgrounds for iPad。 可是,顯然,playground不可能徹底代替Xcode IDE,當開發結束的時候,須要到真實的設備上測試功能app。 最終它只是做爲一個工具被使用,可是這是一個頗有用的工具。
Envato藝雲臺是數據資產和創造性人才匯聚的全球領先市場平臺。全球數百萬人都選擇經過咱們的市場平臺、工做室和課程來購買文件、選聘自由職業者,或者學習建立網站、製做視頻、應用、製圖等所需的技能。咱們的子網站包括Envato藝雲臺Tuts+ 網絡,全球最大的H五、PS、插圖、代碼和攝影教程資源庫,以及Envato藝雲臺市場,其中的900多萬類數字資產均經過如下七大平臺進行銷售 - CodeCanyon、ThemeForest、GraphicRiver、VideoHive、PhotoDune、AudioJungle和3DOcean。