📜 ⬆️ ⬇️

We repeat the design of applications that have received Apple award

Hi, Habr! Designers draw applications with beautiful buttons, shadows, animations, gradients and complex transitions between screens. Unfortunately, such designs are not easy to turn into working applications. Is it possible to facilitate our work? Let's look at examples of applications that have received Apple awards for design: Auxy, Streaks and Zova.

image

This article is for educational purposes only. Please do not use the source code for other purposes.

To create the interface, I will take the Macaw library, which describes the graphics in the form of high-level scene objects. I advise you to look at “ Getting Started ”, if you have not yet seen. Let's go!
')

Streaks


Streaks - To-Do list that cultivates good habits: read every day and do not forget to brush your teeth. Let's define graphic components and connections between them.

image

The first element draws a 2 by 3 grid: the X-coordinate of the first column is zero, the second is half the screen width. The y-coordinate depends on the row number and is equal to row*screen.width/2 (square cells).

The element "streak" includes content and title. On click, the user switches the content between the logo, calendar and statistics. The switching function will be done later.

image

 // grid cell (column, row) let streak = Group( contents: [self.streak(text: "MEDIUM", imageName: "medium")], place: Transform.move( dx: Double(screen.width / 2) * column, dy: Double(screen.width / 2) * row ) ) // streak: content + title func streak(text: String, imageName: String) -> Group { let title = Text( text: text, font: Font(name: fontName, size: 14), fill: Color.white, align: .mid, place: Transform.move( dx: Double(screen.width) / 4, dy: 0.7 * Double(screen.width) / 2 ) ) let streakContent = Group() return [streakContent, title].group() } 

Let's take a logo, calendar and statistics. The logo consists of an arc length of 2 * PI and a picture in the center. The radius depends on the size of the screen. Elements are positioned relative to the point (0,0).

image

 let ellipse = Ellipse(cx: radius, cy: radius, rx: radius, ry: radius) let border = Shape( form: Arc(ellipse: ellipse, extent: 2 * M_PI), fill: background, stroke: Stroke(fill: Color(val: 0x744641), width: 8) ) let image = UIImage(named: imageName)! let logoImage = Image( src: imageName, place: Transform.move( // move image in the point (radius, radius) dx: radius - Double(image.size.width) / 2, dy: radius - Double(image.size.height) / 2 ) ) let logo = [border, logoImage].group() 

The calendar includes the name of the month, day of the week, and the status of days: “done,” “missed,” or “to be done.” “Made” and “to be” are displayed in simple circles. “Missing” - two intersecting lines of black color.

image

 // input parameters let x = width / 6 * Double(column) let y = Double(row) * 15 // skip day let line1 = Line(x1: x - 4, y1: y, x2: x + 4, y2: y + 8) let line2 = Line(x1: x - 4, y1: y + 8, x2: x + 4, y2: y) let stroke = Stroke(fill: Color.black, width: 4) let cross = [ Shape(form: line1, stroke: stroke), Shape(form: line2, stroke: stroke) ].group() // done day let done = Shape( form: Circle(cx: x, cy: y + radius, r: radius), fill: doneColor ) // future day let future = Shape( form: Circle(cx: x, cy: y + radius, r: radius), fill: lightColor ) 

Statistics is a group of three bars with different Y-coordinates. The Y-coordinate depends on the bar number, and the width of the bar depends on the size of the screen: in our case it is 80% of half the width (10% indent on each side).

The bar contains four elements: two texts and two rectangles with rounded corners. One rectangle filled.

image

 let bar = Group(contents: [ Text( text: "LAST 30 DAYS", font: Font(name: fontName, size: 12), fill: Color.white, align: .min ), Text( text: "42%", font: Font(name: fontName, size: 12), fill: lightColor, align: .max, place: Transform.move(dx: width, dy: 0) ), Shape( form: Rect(x: 0, y: 18, w: width, h: 10).round(r: 2), fill: lightColor ), Shape( form: Rect(x: 0, y: 18, w: width * 0.42, h: 10).round(r: 2), fill: Color.white ) ] 

Finished with content, move on to the animation switch. Tap on streak'u switches and centers the new content (elements differ in width). The first animation hides the old content, the second shows the new.

 func animateStreak(newContent: Group, margin: Int) { let animation = streakContent.opacityVar.animation(to: 0.0, during: 0.1) animation.onComplete { streakContent.contents = newContent streakContent.place = Transform.move(dx: margin / 2, dy: 0) streakContent.opacityVar.animation(to: 1.0, during: 0.1).play() } animation.play() } 

Stayed the final touch. We start arc animation from 1.5*PI to 3.5*PI when creating a new habit. Here we read more about content-animation. At the end of the animation, open the “Add Task” controller.

 streak.onTap { tapEvent in let animation = group.contentsVar.animation({ t in let animatedShape = Shape( form: Arc(ellipse: ellipse, shift: 1.5 * M_PI, extent: 2 * M_PI * t), stroke: Stroke(fill: Color.white, width: 8) ) return [animatedShape] }, during: 0.5).easing(Easing.easeInOut) animation.onComplete { // open task controller } animation.play() } 

Result


Do not forget to brush your teeth and look at the Xcode project on GitHub.

image

Auxy studio


Auxy is a studio for making music and beats on the phone. Blue squares are sounds, the user adds and deletes them by tap. When you click on "Play", the white line moves from top to bottom and reproduces it when crossing with sound.

image

Auxy screen consists of four main components: the Play button, LineRunner, sounds and a grid in the background.

The grid contains horizontal and vertical lines. Every fourth horizontal line is highlighted. The grid size is 8x16: the cell width of screen.width / columns and the height of screen.height / rows .

image

 let columns = Array(0..<dimension.0).map { column in let x = cell.w * column return Shape( form: Line(x1: x, y1: 0, x2: x, y2: size.h), stroke: stroke, opacity: 0.2 ) }.group() let rows = Array(0..<dimension.1).map { row in let y = cell.h * row return Shape( form: Line(x1: 0, y1: y, x2: size.w, y2: y), stroke: stroke, opacity: row % 4 == 0 ? 1 : 0.2 ) }.group() 

When tapes on the screen add sound to the grid. The column and line are calculated from the coordinates of the tapa. The sound contains two rectangles: the front is white with transparency 0.0 and the back is blue. The front rectangle lights up when the line crosses the sound.

 let column = floor(tapLocation.x / cellSize.w) let row = floor(tapLocation.y / cellSize.h) let rect = Rect(w: cellSize.w, h: cellSize.h) let background = Shape(form: rect, fill: Color.rgb(r: 4, g: 112, b: 215)) let foreground = Shape(form: rect, fill: Color.white, opacity: 0.0) let sound = Group( contents: [background, foreground], place: Transform.move( dx: column * cellSize.w, dy: row * cellSize.h ) ) 

The “Play” button is the most difficult element. Two static elements: a filled circle and an indented arc of 0.05 near PI/2 . "Play" consists of three points: (-1, 2), (2, 0), (-1, -2), "Stop" of four: (-2, 2), (2, 2), (2 , -2), (-2, -2). Any element of the scene is easily scaled to the desired size. Vector graphics - power!



 let border = Shape( form: Arc( ellipse: Ellipse(rx: radius, ry: radius), shift: -M_PI / 2 + 0.05, extent: 2 * M_PI - 0.1 ), stroke: Stroke(fill: Color.rgba(r: 219, g: 222, b: 227, a: 0.3), width: 2.0) ) let circle = Shape(form: Circle(r: 25.0), fill: olor) let playButton = Shape( form: MoveTo(x: -1, y: 2).lineTo(x: 2, y: 0) .lineTo(x: -1, y: -2).close().build(), fill: Color.rgb(r: 46, g: 48, b: 58), place: Transform.scale(sx: 5.0, sy: 5.0) ) let stopButton = Shape( form: MoveTo(x: -2, y: 2).lineTo(x: 2, y: 2) .lineTo(x: 2, y: -2).lineTo(x: -2, y: -2).close().build(), fill: Color.rgb(r: 46, g: 48, b: 58), place: Transform.scale(sx: 4.0, sy: 4.0) ) let buttons = [[playButton], [stopButton]] let buttonGroup = Group(contents: buttons[0]) let button = Group(contents: [border, circle, buttonGroup]) 

When the user clicks "Play":

 button.onTap { tapEvent in // change button content let index = buttons.index { $0 == buttonGroup.contents }! buttonGroup.contents = buttons[(index + 1) % buttons.count] if index == 0 { play() } else { // if stop pressed contentAnimation.stop() // hide animation group animationGroup.opacityVar.animation(to: 0.0, during: 0.1).play() } } func play() { contentAnimation = animationGroup.contentsVar.animation({ t in let shape = Shape( form: Arc( ellipse: Ellipse(rx: radius, ry: radius), shift: -M_PI / 2 + 0.05, extent: max(2 * M_PI * t - 0.1, 0) ), stroke: Stroke(fill: Color.white, width: 2) ) return [shape] }, during: time).cycle() contentAnimation.play() } 

When you click on the "play" line moves cyclically from top to bottom. When intersecting with sound, we highlight it. Knowing the time of the animation, we calculate when the line crosses the sound. This value is the delay of the highlight animation.



 let line = Shape( form: Line(x1: 0, y1: 0, x2: size.w, y2: 0), stroke: Stroke(fill: Color.rgba(r: 219, g: 222, b: 227, a: 0.5), width: 1.0) ) func run(time: Double) { let lineAnimation = line.placeVar.animation( to: Transform.move(dx: 0, dy: screen.height), during: time ).easing(Easing.linear) let hightlight = sounds.map { sound -> Animation in return sound.hightlight().delay(sound.place.dy / screen.height * time) }.combine() let runAnimation = [soundsAnimation, lineAnimation].combine().cycle() runAnimation?.play() } 

Result


Create music and watch the Xcode project on GitHub.



Zova


Zova is a personal fitness trainer. It has two components: a pie chart in the center and a bar at the bottom of the screen.



The pie chart includes eight circles in the background, one filled circle in the center, live score and an emoji icon.



 let mainCircle = Shape( form: Circle(r: 60), fill: mainColor, stroke: Stroke(fill: Color.white, width: 1.0) ) let score = Text( text: "3", font: Font(name: lightFont, size: 40), fill: Color.white, align: .mid, baseline: .mid ) let icon = Text( text: "", font: Font(name: regularFont, size: 24), fill: Color.white, align: .mid, place: Transform.move(dx: 0.0, dy: 30.0) ) let shadows = [ Point(x: 0, y: 35), Point(x: -25, y: 25), Point(x: 25, y: 25), Point(x: 25, y: -25), Point(x: -25, y: -25), Point(x: -40, y: 0), Point(x: 40, y: 0), Point(x: 0, y: -35) ].map { place in return Shape( form: Circle(r: 40), fill: Color.white.with(a: 0.8), place: Transform.move(dx: place.x, dy: place.y) ) }.group() let acivityCircle = Group(contents: [shadows, mainCircle, score, icon]) 

Tap in a pie chart displays available emoji icons in a circle. If the distance from the center to the icon d, then the coordinates of the icon (cos(alpha) * d, sin(alpha) * d) . By default, the icon selection menu is hidden (transparency 0.0).


 let data = ["", "", ""] //      emoji let emojis = data.enumerated().map { (index, item) -> Group in let shape = Shape(form: Circle(r: 20), fill: Color.white) let icon = Text( text: item, font: Font(name: regularFont, size: 14), fill: Color.white, align: .mid, baseline: .mid ) return Group( contents: [shape, icon], place: emojiPlace(index: index, d: 20.0), opacity: 0.0 ) }.group() func emojiPlace(index: Int, d: Double) -> Transform { let alpha = 2 * M_PI / 10.0 * Double(index) return Transform.move( dx: cos(alpha) * d, dy: sin(alpha) * d ) } 

Bar - a group consisting of legends and segments. The legend consists of a rounded rectangle, a “low” text, and another rectangle with a gradient color.



 let border = Shape( form: Rect(w: 80, h: 30).round(r: 16.0), fill: Color.white ) let text = Text( text: "Low", font: Font(name: regularFont, size: 20), fill: mainColor, align: .mid, baseline: .mid, place: Transform.move(dx: 40, dy: 15) ) let line = Shape( form: Rect(x: 20, y: 30, w: 2, h: 40), fill: LinearGradient( degree: 90, from: Color.white.with(a: 0.8), to: mainColor ) ) let legend = [border, text, line].group() 

A segment consists of a rectangle and a text for us. The x-coordinate of the elements is zero. The x-coordinate of a segment depends on its number. The last segment has a gradient color.

 let text = Text( text: text, font: Font(name: regularFont, size: 12), fill: Color.white, align: .min, baseline: .alphabetic, place: Transform.move(dx: 0, dy: -5) ) let rect = Shape( form: Rect(w: width, h: 8), fill: !last ? color : gradient ) let bar = [text, rect].group() 

The legend has a “jumping” effect: it slowly moves up and down along the vertical axis.

 let jumpAnimation = legend.placeVar.animation( to: Transform.move(dx: 0.0, dy: -8.0), during: 2.0 ).autoreversed().cycle() 

Let's go back to the animation. When tapping a pie chart, we run several animations at the same time:


Good news! We do not need to worry about reverse animation, it is available automatically: any animation has a reverse() method.



 let during = 0.5 let hideAnimation = [ bar.opacityVar.animation(to: 0.0, during: during), texts.opacityVar.animation(to: 0.0, during: during) ].combine() let emojisAnimation = emojis.contents.enumerated().map { (index, node) in return [ node.opacityVar.animation(to: 1.0, during: during), node.placeVar.animation( // new emoji position to: emojiPlace(index: index, d: 120.0), during: during ) ].combine() }.combine() let circleAnimation = [ shadows.placeVar.animation(to: Transform.scale(sx: 0.5, sy: 0.5), during: during), score.placeVar.animation(to: Transform.move(dx: 0, dy: -20), during: during), score.opacityVar.animation(to: 0.0, during: during), icon.placeVar.animation(to: Transform.move(dx: 0, dy: -30).scale(sx: 2.0, sy: 2.0), during: during), ].combine() let animation = [hideAnimation, emojisAnimation, circleAnimation].combine() let reverseAnimation = animation.reverse() 

Result


Doing sports and watching the Xcode project on GitHub.



Summary


Reviving the design, and even more so the design that won the Apple award, is not an easy job. Developers spend a lot of time making their own graphic elements and animations that work on devices of various sizes. This work can be simplified using tools that provide the right abstractions and a convenient API. Macaw is one such library that allows you to focus on the main thing .

Source: https://habr.com/ru/post/323308/


All Articles