17 분 소요

📝 학습 목표


  1. HIG(Human Interface Guidelines)에 따른 애니메이션을 사용하는 이유를 이해한다.
  2. 애니메이션을 나타내기 위한 2가지 방법을 이해한다.
  3. UIView의 animate 메소드를 이해하고 사용할 수 있다.
  4. Gesture를 이해하고 기능들을 활용하여 애니메이션을 구현할 수 있다.

✍🏻 수업 내용 정리


애니메이션 사용하는 경우

  1. 시스템 상태를 나타내기 위해

    1

  2. 메뉴 및 전환을 나타내기 위해

    2

  3. 시각적인 피드백을 주기 위해

    3

애니메이션을 나타내기 위한 2가지 방법

1.UIView의 animate 활용하기(IOS 4~)

2.Core Animation 사용하기(IOS 10~)

Gesture

터치 액션은 버튼으로만 할 뿐만 아니라 제스쳐를 통해 할 수 있도록 한다.

예를 들어 채팅 끝나고 키보드 내릴 때, 빈 화면 터치하는 경우

아이폰 배경에서 아래로 스와이프하면 검색 뜨는 거 4

5

🎯 핵심 키워드


  • Animation

    # animation(_:)

    Applies the given animation to all animatable values within this view.

    iOS 13.0–15.0 DeprecatediPadOS 13.0–15.0 DeprecatedmacOS 10.15–12.0 DeprecatedMac Catalyst 13.0–15.0 DeprecatedtvOS 13.0–15.0 DeprecatedwatchOS 6.0–8.0 Deprecated

    Deprecated Use withAnimation or animation(_:value:) instead.

    ## Declaration

    func animation(_ animation: [Animation](https://developer.apple.com/documentation/swiftui/animation)?) -> some [View](https://developer.apple.com/documentation/swiftui/view)

    ## Return Value

    A view that wraps this view and applies animation to all animatable values used within the view.

    **ParametersanimationThe animation to apply to animatable values within this view.

    ## Discussion

    Use this modifier on leaf views rather than container views. The animation applies to all child views within this view; calling animation(_:) on a container view can lead to unbounded scope.

  • Gesture

    # Gesture

    An instance that matches a sequence of events to a gesture, and returns a stream of values for each of its states.

    iOS 13.0+iPadOS 13.0+macOS 10.15+Mac Catalyst 13.0+tvOS 13.0+watchOS 6.0+

    ## Declaration

    protocol Gesture

    ## Overview

    Create custom gestures by declaring types that conform to the Gesture protocol.

    ## Topics

    **Implementing a custom gesture**

    [var body: Self.Body](https://developer.apple.com/documentation/swiftui/gesture/body-swift.property)

    The content and behavior of the gesture.

    Required.

    [associatedtype Body : Gesture](https://developer.apple.com/documentation/swiftui/gesture/body-swift.associatedtype)

    The type of gesture representing the body of Self.

    Required.

    **Performing the gesture**

    [func updating<State>(GestureState<State>, body: (Self.Value, inout State, inout Transaction) -> Void) -> GestureStateGesture<Self, State>](https://developer.apple.com/documentation/swiftui/gesture/updating(_:body:))

    Updates the provided gesture state property as the gesture’s value changes.

    [func onChanged((Self.Value) -> Void) -> _ChangedGesture<Self>](https://developer.apple.com/documentation/swiftui/gesture/onchanged(_:))

    Adds an action to perform when the gesture’s value changes.

    Available when Value conforms to Equatable.

    [func onEnded((Self.Value) -> Void) -> _EndedGesture<Self>](https://developer.apple.com/documentation/swiftui/gesture/onended(_:))

    Adds an action to perform when the gesture ends.

    [associatedtype Value](https://developer.apple.com/documentation/swiftui/gesture/value)

    The type representing the gesture’s value.

    Required.

    **Composing gestures**

    [func simultaneously<Other>(with: Other) -> SimultaneousGesture<Self, Other>](https://developer.apple.com/documentation/swiftui/gesture/simultaneously(with:))

    Combines a gesture with another gesture to create a new gesture that recognizes both gestures at the same time.

    [func sequenced<Other>(before: Other) -> SequenceGesture<Self, Other>](https://developer.apple.com/documentation/swiftui/gesture/sequenced(before:))

    Sequences a gesture with another one to create a new gesture, which results in the second gesture only receiving events after the first gesture succeeds.

    [func exclusively<Other>(before: Other) -> ExclusiveGesture<Self, Other>](https://developer.apple.com/documentation/swiftui/gesture/exclusively(before:))

    Combines two gestures exclusively to create a new gesture where only one gesture succeeds, giving precedence to the first gesture.

    **Adding modifier keys to a gesture**

    [func modifiers(EventModifiers) -> _ModifiersGesture<Self>](https://developer.apple.com/documentation/swiftui/gesture/modifiers(_:))

    Combines a gesture with keyboard modifiers.

    **Transforming a gesture**

    [func map<T>((Self.Value) -> T) -> _MapGesture<Self, T>](https://developer.apple.com/documentation/swiftui/gesture/map(_:))

    Returns a gesture that’s the result of mapping the given closure over the gesture.

    ## Relationships

    **Conforming Types**

    • [AnyGesture](https://developer.apple.com/documentation/swiftui/anygesture)
    • [DragGesture](https://developer.apple.com/documentation/swiftui/draggesture)
    • [ExclusiveGesture](https://developer.apple.com/documentation/swiftui/exclusivegesture)
    • [GestureStateGesture](https://developer.apple.com/documentation/swiftui/gesturestategesture)
    • [LongPressGesture](https://developer.apple.com/documentation/swiftui/longpressgesture)
    • [MagnificationGesture](https://developer.apple.com/documentation/swiftui/magnificationgesture)
    • [RotationGesture](https://developer.apple.com/documentation/swiftui/rotationgesture)
    • [SequenceGesture](https://developer.apple.com/documentation/swiftui/sequencegesture)
    • [SimultaneousGesture](https://developer.apple.com/documentation/swiftui/simultaneousgesture)
    • [SpatialTapGesture](https://developer.apple.com/documentation/swiftui/spatialtapgesture)
    • [TapGesture](https://developer.apple.com/documentation/swiftui/tapgesture)
  • UIGestureRecognizer

    # UIGestureRecognizer

    The base class for concrete gesture recognizers.

    iOS 3.2+iPadOS 3.2+Mac Catalyst 13.1+tvOS 9.0+

    ## Declaration

    @MainActor class UIGestureRecognizer : [NSObject](https://developer.apple.com/documentation/objectivec/nsobject)

    ## Overview

    gesture recognizer decouples the logic for recognizing a sequence of touches (or other input) and acting on that recognition. When one of these objects recognizes a common gesture or, in some cases, a change in the gesture, it sends an action message to each designated target object.

    The concrete subclasses of UIGestureRecognizer are the following:

    • [UITapGestureRecognizer](https://developer.apple.com/documentation/uikit/uitapgesturerecognizer)
    • [UIPinchGestureRecognizer](https://developer.apple.com/documentation/uikit/uipinchgesturerecognizer)
    • [UIRotationGestureRecognizer](https://developer.apple.com/documentation/uikit/uirotationgesturerecognizer)
    • [UISwipeGestureRecognizer](https://developer.apple.com/documentation/uikit/uiswipegesturerecognizer)
    • [UIPanGestureRecognizer](https://developer.apple.com/documentation/uikit/uipangesturerecognizer)
    • [UIScreenEdgePanGestureRecognizer](https://developer.apple.com/documentation/uikit/uiscreenedgepangesturerecognizer)
    • [UILongPressGestureRecognizer](https://developer.apple.com/documentation/uikit/uilongpressgesturerecognizer)
    • [UIHoverGestureRecognizer](https://developer.apple.com/documentation/uikit/uihovergesturerecognizer)

    The UIGestureRecognizer class defines a set of common behaviors that can be configured for all concrete gesture recognizers. It can also communicate with its delegate (an object that adopts the [UIGestureRecognizerDelegate](https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate) protocol), thereby enabling finer-grained customization of some behaviors.

    A gesture recognizer operates on touches hit-tested to a specific view and all of that view’s subviews. It thus must be associated with that view. To make that association you must call the [UIView](https://developer.apple.com/documentation/uikit/uiview) method [addGestureRecognizer(_:)](https://developer.apple.com/documentation/uikit/uiview/1622496-addgesturerecognizer). A gesture recognizer doesn’t participate in the view’s responder chain.

    A gesture recognizer has one or more target-action pairs associated with it. If there are multiple target-action pairs, they’re discrete, and not cumulative. Recognition of a gesture results in the dispatch of an action message to a target for each of the associated pairs. The action methods invoked must conform to one of the following signatures:

    @IBAction func myActionMethod()@IBAction func myActionMethod(_ sender: UIGestureRecognizer)

    Methods conforming to the latter signature permit the target in some cases to query the gesture recognizer sending the message for additional information. For example, the target could ask a [UIRotationGestureRecognizer](https://developer.apple.com/documentation/uikit/uirotationgesturerecognizer) object for the angle of rotation (in radians) since the last invocation of the action method for this gesture. Clients of gesture recognizers can also ask for the location of a gesture by calling [location(in:)](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624219-location) or [location(ofTouch:in:)](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624201-location).

    The gesture interpreted by a gesture recognizer can be either discrete or continuous. A discrete gesture, such as a double tap, occurs but once in a multi-touch sequence and results in a single action sent. However, when a gesture recognizer interprets a continuous gesture such as a rotation gesture, it sends an action message for each incremental change until the multi-touch sequence concludes.

    A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are canceled. The usual sequence of actions in gesture recognition follows a path determined by default values of the [cancelsTouchesInView](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624218-cancelstouchesinview)[delaysTouchesBegan](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624234-delaystouchesbegan)[delaysTouchesEnded](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624209-delaystouchesended) properties:

    • cancelsTouchesInView — If a gesture recognizer recognizes its gesture, it unbinds the remaining touches of that gesture from their view (so the window won’t deliver them). The window cancels the previously delivered touches with a ([touchesCancelled(_:with:)](https://developer.apple.com/documentation/uikit/uiresponder/1621116-touchescancelled)) message. If a gesture recognizer doesn’t recognize its gesture, the view receives all touches in the multi-touch sequence.
    • delaysTouchesBegan — As long as a gesture recognizer, when analyzing touch events, hasn’t failed recognition of its gesture, the window withholds delivery of touch objects in the [UITouch.Phase.began](https://developer.apple.com/documentation/uikit/uitouch/phase/began) phase to the attached view. If the gesture recognizer subsequently recognizes its gesture, the view doesn’t receive these touch objects. If the gesture recognizer doesn’t recognize its gesture, the window delivers these objects in an invocation of the view’s [touchesBegan(_:with:)](https://developer.apple.com/documentation/uikit/uiresponder/1621142-touchesbegan) method (and possibly a follow-up [touchesMoved(_:with:)](https://developer.apple.com/documentation/uikit/uiresponder/1621107-touchesmoved) invocation to inform it of the touches current location).
    • delaysTouchesEnded — As long as a gesture recognizer, when analyzing touch events, hasn’t failed recognition of its gesture, the window withholds delivery of touch objects in the [UITouch.Phase.ended](https://developer.apple.com/documentation/uikit/uitouch/phase/ended) phase to the attached view. If the gesture recognizer subsequently recognizes its gesture, the touches are canceled (in a [touchesCancelled(_:with:)](https://developer.apple.com/documentation/uikit/uiresponder/1621116-touchescancelled) message). If the gesture recognizer doesn’t recognize its gesture, the window delivers these objects in an invocation of the view’s [touchesEnded(_:with:)](https://developer.apple.com/documentation/uikit/uiresponder/1621084-touchesended) method.

    Note that “recognize” in the above descriptions doesn’t necessarily equate to a transition to the Recognized state.

    ### Subclassing notes

    You may create a subclass of UIGestureRecognizer that recognizes a distinctive gesture — for example, a “check mark” gesture. If you’re going to create such a concrete gesture recognizer, be sure to import the UIGestureRecognizerSubclass.h header file (for Objective-C) or the UIKit.UIGestureRecognizerSubclass module (for Swift). This file declares all the methods and properties a subclass must either override, call, or reset.

    Gesture recognizers operate within a predefined state machine, transitioning to subsequent states as they handle multi-touch events. The states and their possible transitions differ for continuous and discrete gestures. All gesture recognizers begin a multi-touch sequence in the Possible state ([UIGestureRecognizer.State.possible](https://developer.apple.com/documentation/uikit/uigesturerecognizer/state/possible)). Discrete gestures transition from Possible to either Recognized ([recognized](https://developer.apple.com/documentation/uikit/uigesturerecognizer/state/1624228-recognized)) or Failed ([UIGestureRecognizer.State.failed](https://developer.apple.com/documentation/uikit/uigesturerecognizer/state/failed)), depending on whether they successfully interpret the gesture or not. If the gesture recognizer transitions to Recognized, it sends its action message to its target.

    For continuous gestures, the state transitions a gesture recognizer might make are more numerous, as indicated in the following sequence:

    • Possible —> Began —> [Changed] —> Cancelled
    • Possible —> Began —> [Changed] —> Ended

    The Changed state is optional and may occur multiple times before the Cancelled or Ended state is reached. The gesture recognizer sends action messages at each state transition. Thus for a continuous gesture such as a pinch, action messages are sent as the two fingers move toward or away from each other. The enum constants representing these states are of type [UIGestureRecognizer.State](https://developer.apple.com/documentation/uikit/uigesturerecognizer/state). (Note that the constants for Recognized and Ended states are synonymous.)

    Subclasses must set the [state](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1619998-state) property to the appropriate value when they transition between states.

    ### Methods to override

    The methods that subclasses must override are described in Implementing subclasses. Subclasses must also periodically reset the [state](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1619998-state) property (as described above) and may call the [ignore(_:for:)](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1620010-ignore) method.

    ### Special considerations

    The [state](https://developer.apple.com/documentation/uikit/uigesturerecognizer/1619998-state) property is declared in UIGestureRecognizer.h as being read-only. This property declaration is intended for clients of gesture recognizers. Subclasses of UIGestureRecognizer must import the UIGestureRecognizerSubclass.h header file (for Objective-C) or the UIKit.UIGestureRecognizerSubclass module (for Swift). This file contains a redeclaration of state that makes it read-write.

  • layoutIfNeeded()

    # layoutIfNeeded()

    Recalculate the receiver’s layout, if required.

    iOS 2.0+iPadOS 2.0+macOS 10.5+Mac Catalyst 13.0+tvOS 9.0+

    ## Declaration

    func layoutIfNeeded()

    ## Discussion

    When this message is received, the layer’s super layers are traversed until a ancestor layer is found that does not require layout. Then layout is performed on the entire layer-tree beneath that ancestor.

    ## See Also

    **Managing Layer Resizing and Layout**

    [var layoutManager: CALayoutManager?](https://developer.apple.com/documentation/quartzcore/calayer/1410749-layoutmanager)

    The object responsible for laying out the layer’s sublayers.

    [func setNeedsLayout()](https://developer.apple.com/documentation/quartzcore/calayer/1410946-setneedslayout)

    Invalidates the layer’s layout and marks it as needing an update.

    [func layoutSublayers()](https://developer.apple.com/documentation/quartzcore/calayer/1410935-layoutsublayers)

    Tells the layer to update its layout.

    [func needsLayout() -> Bool](https://developer.apple.com/documentation/quartzcore/calayer/1410956-needslayout)

    Returns a Boolean indicating whether the layer has been marked as needing a layout update.

    [var autoresizingMask: CAAutoresizingMask](https://developer.apple.com/documentation/quartzcore/calayer/1410877-autoresizingmask)

    A bitmask defining how the layer is resized when the bounds of its superlayer changes.

    [func resize(withOldSuperlayerSize: CGSize)](https://developer.apple.com/documentation/quartzcore/calayer/1410894-resize)

    Informs the receiver that the size of its superlayer changed.

    [func resizeSublayers(withOldSize: CGSize)](https://developer.apple.com/documentation/quartzcore/calayer/1410929-resizesublayers)

    Informs the receiver’s sublayers that the receiver’s size has changed.

    [func preferredFrameSize() -> CGSize](https://developer.apple.com/documentation/quartzcore/calayer/1410980-preferredframesize)

    Returns the preferred size of the layer in the coordinate space of its superlayer.

  • addTarget

    # addTarget(_:action:for:)

    AppIntentsUIKitiOS 16.0+iPadOS 16.0+macOS 13.0+Mac Catalyst 16.0+tvOS 16.0+watchOS 9.0+

    ## Declaration

    override final func addTarget( _ target: Any?, action: [Selector](https://developer.apple.com/documentation/objectivec/selector), for controlEvents: [UIControl](https://developer.apple.com/documentation/uikit/uicontrol).[Event](https://developer.apple.com/documentation/uikit/uicontrol/event) )

    • dictionary

      # Dictionary

      A collection whose elements are key-value pairs.

      iOS 8.0+iPadOS 8.0+macOS 10.10+Mac Catalyst 13.0+tvOS 9.0+watchOS 2.0+

      ## Declaration

      @frozen struct Dictionary<Key, Value> where Key : [Hashable](https://developer.apple.com/documentation/swift/hashable)

      ## Overview

      A dictionary is a type of hash table, providing fast access to the entries it contains. Each entry in the table is identified using its key, which is a hashable type such as a string or number. You use that key to retrieve the corresponding value, which can be any object. In other languages, similar data types are known as hashes or associated arrays.

      Create a new dictionary by using a dictionary literal. A dictionary literal is a comma-separated list of key-value pairs, in which a colon separates each key from its associated value, surrounded by square brackets. You can assign a dictionary literal to a variable or constant or pass it to a function that expects a dictionary.

      Here’s how you would create a dictionary of HTTP response codes and their related messages:

      var responseMessages = [200: "OK", 403: "Access forbidden", 404: "File not found", 500: "Internal server error"]

      The responseMessages variable is inferred to have type [Int: String]. The Key type of the dictionary is Int, and the Value type of the dictionary is String.

      To create a dictionary with no key-value pairs, use an empty dictionary literal ([:]).

      var emptyDict: [String: String] = [:]

      Any type that conforms to the Hashable protocol can be used as a dictionary’s Key type, including all of Swift’s basic types. You can use your own custom types as dictionary keys by making them conform to the Hashable protocol.

      # Getting and Setting Dictionary Values

      The most common way to access values in a dictionary is to use a key as a subscript. Subscripting with a key takes the following form:

      print(responseMessages[200])// Prints "Optional("OK")"

      Subscripting a dictionary with a key returns an optional value, because a dictionary might not hold a value for the key that you use in the subscript.

      The next example uses key-based subscripting of the responseMessages dictionary with two keys that exist in the dictionary and one that does not.

      let httpResponseCodes = [200, 403, 301]for code in httpResponseCodes { if let message = responseMessages[code] { print("Response \(code): \(message)") } else { print("Unknown response \(code)") }}// Prints "Response 200: OK"// Prints "Response 403: Access forbidden"// Prints "Unknown response 301"

      You can also update, modify, or remove keys and values from a dictionary using the key-based subscript. To add a new key-value pair, assign a value to a key that isn’t yet a part of the dictionary.

      responseMessages[301] = "Moved permanently"print(responseMessages[301])// Prints "Optional("Moved permanently")"

      Update an existing value by assigning a new value to a key that already exists in the dictionary. If you assign nil to an existing key, the key and its associated value are removed. The following example updates the value for the 404 code to be simply “Not found” and removes the key-value pair for the 500 code entirely.

      responseMessages[404] = "Not found"responseMessages[500] = nilprint(responseMessages)// Prints "[301: "Moved permanently", 200: "OK", 403: "Access forbidden", 404: "Not found"]"

      In a mutable Dictionary instance, you can modify in place a value that you’ve accessed through a keyed subscript. The code sample below declares a dictionary called interestingNumbers with string keys and values that are integer arrays, then sorts each array in-place in descending order.

      var interestingNumbers = ["primes": [2, 3, 5, 7, 11, 13, 17], "triangular": [1, 3, 6, 10, 15, 21, 28], "hexagonal": [1, 6, 15, 28, 45, 66, 91]]for key in interestingNumbers.keys { interestingNumbers[key]?.sort(by: >)} print(interestingNumbers["primes"]!)// Prints "[17, 13, 11, 7, 5, 3, 2]"

      # Iterating Over the Contents of a Dictionary

      Every dictionary is an unordered collection of key-value pairs. You can iterate over a dictionary using a for-in loop, decomposing each key-value pair into the elements of a tuple.

      let imagePaths = ["star": "/glyphs/star.png", "portrait": "/images/content/portrait.jpg", "spacer": "/images/shared/spacer.gif"] for (name, path) in imagePaths { print("The path to '\(name)' is '\(path)'.")}// Prints "The path to 'star' is '/glyphs/star.png'."// Prints "The path to 'portrait' is '/images/content/portrait.jpg'."// Prints "The path to 'spacer' is '/images/shared/spacer.gif'."

      The order of key-value pairs in a dictionary is stable between mutations but is otherwise unpredictable. If you need an ordered collection of key-value pairs and don’t need the fast key lookup that Dictionary provides, see the KeyValuePairs type for an alternative.

      You can search a dictionary’s contents for a particular value using the contains(where:) or firstIndex(where:) methods supplied by default implementation. The following example checks to see if imagePaths contains any paths in the "/glyphs" directory:

      let glyphIndex = imagePaths.firstIndex(where: { $0.value.hasPrefix("/glyphs") })if let index = glyphIndex { print("The '\(imagePaths[index].key)' image is a glyph.")} else { print("No glyphs found!")}// Prints "The 'star' image is a glyph.")

      Note that in this example, imagePaths is subscripted using a dictionary index. Unlike the key-based subscript, the index-based subscript returns the corresponding key-value pair as a non-optional tuple.

      print(imagePaths[glyphIndex!])// Prints "(key: "star", value: "/glyphs/star.png")"

      A dictionary’s indices stay valid across additions to the dictionary as long as the dictionary has enough capacity to store the added values without allocating more buffer. When a dictionary outgrows its buffer, existing indices may be invalidated without any notification.

      When you know how many new values you’re adding to a dictionary, use the init(minimumCapacity:) initializer to allocate the correct amount of buffer.

      # Bridging Between Dictionary and NSDictionary

      You can bridge between Dictionary and NSDictionary using the as operator. For bridging to be possible, the Key and Value types of a dictionary must be classes, @objc protocols, or types that bridge to Foundation types.

      Bridging from Dictionary to NSDictionary always takes O(1) time and space. When the dictionary’s Key and Value types are neither classes nor @objc protocols, any required bridging of elements occurs at the first access of each element. For this reason, the first operation that uses the contents of the dictionary may take O(n).

      Bridging from NSDictionary to Dictionary first calls the copy(with:) method (- copyWithZone: in Objective-C) on the dictionary to get an immutable copy and then performs additional Swift bookkeeping work that takes O(1) time. For instances of NSDictionary that are already immutable, copy(with:) usually returns the same dictionary in O(1) time; otherwise, the copying performance is unspecified. The instances of NSDictionary and Dictionary share buffer using the same copy-on-write optimization that is used when two instances of Dictionary share buffer.

  • completion

    nstance Property completion

    The completion which will be sent to a Subscriber.

    iOS 13.0+iPadOS 13.0+macOS 10.15+Mac Catalyst 13.0+tvOS 13.0+watchOS 6.0+

    ## Declaration

    var completion: [Subscribers](https://developer.apple.com/documentation/combine/subscribers).[Completion](https://developer.apple.com/documentation/combine/subscribers/completion)<Failure> { get }

📢 8주차 수업 후기


  • 애니메이션은 앱을 만들 때 앱의 컨셉에 따라 엄청나게 필요하게 되는 테크라고 생각을 한다.

    컴포넌트가 내 의지대로 움직이는 것을 보며 상당히 재미있었다.

⚠️ 스터디간 주의사항


  1. 과제 피드백 기반 진행입니다 - 한명씩 본인의 과제를 발표하는 시간 그리고 해온 과제에 대한 피드백을 하는 시간 (ex:전 이렇게 생각해서 이런 부분 다르게 해왔는데 저것도 괜찮은 것 같아요!)이 무조건 기반이 되어야 합니다!
  2. 부가적으로 워크북에서 제공되는 키워드 혹은 강의에서 들은 디테일적인 부분에서 더 토의해봐도 좋을 것 같습니다.

✅ 실습 체크리스트


  • UIView의 animate 메소드를 활용하여 애니메이션을 구현했나요?

    6

  • layoutIfNeeded 메소드를 활용하여 애니메이션을 구현했나요?

    6

  • Tap Gesture를 활용하여 애니메이션을 구현했나요?

    6

  • 미션 참고 영상

    [iOS] #8. Animation - Practice

⚡ 트러블 슈팅


  • ⚡이슈 No.1 (예시, 서식만 복사하시고 지워주세요.)

    이슈

    👉 앱 실행 중에 노래 다음 버튼을 누르니까 앱이 종료되었다.

    문제

    👉 노래클래스의 데이터리스트의 Size를 넘어서 NullPointException이 발생하여 앱이 종료된 것이었다.

    해결

    👉 노래 다음 버튼을 눌렀을 때 데이터리스트의 Size를 검사해 Size보다 넘어가려고 하면 다음으로 넘어가는 메서드를 실행시키지 않고, 첫 노래로 돌아가게끔 해결

    참고 레퍼런스

    • 링크

🤔 이것도 한 번 생각해봐요!


  • UIView.animate의 options에는 어떤 종류들이 있을지 공부해보세요!

  • Core Animation에 대해 공부해보세요!

  • Gesture 여러개가 겹치면 어떻게 될지 공부해보세요!

  • Gesture의 종류에 대해 공부해보세요! (ex. Tap Gesture, Swipe Gesture 등)

  • #selector에 대해서 공부해보세요!

스탠다드 미션

오늘 할 미션은 내 프로젝트에 애니메이션 적용하기

저번에 뽀모도로에서 이미 애니메이션을 사용한 바가 있기 때문에

오늘은 애니메이션을 하나 더 추가해보는 방식으로 진행하고자 한다.

저번 코드에서 애니메이션과 관련되었던 부분은


    
    ***@IBOutlet weak var imageView: UIImageView!***
   
    

    
    func configureToggleButton(){
        self.toggleButton.setTitle("시작", for: .normal)
        self.toggleButton.setTitle("일시정지", for: .selected)
    }
    
    func startTimer() {
        if self.timer == nil {
            self.timer = DispatchSource.makeTimerSource(flags: [], queue: .main)
            self.timer?.schedule(deadline: .now(), repeating: 1)
            self.timer?.setEventHandler(handler: { [weak self] in
                guard let self = self else { return }
                self.currentSeconds -= 1
                let hour = self.currentSeconds / 3600
                let minute = (self.currentSeconds % 3600) / 60
                let seconds = (self.currentSeconds % 3600) % 60
                self.timerLabel.text = String(format: "%02d:%02d:%02d", hour, minute, seconds)
                self.progessView.progress = Float(self.currentSeconds) / Float(self.duration)
                **UIView.animate(withDuration: 0.5, delay: 0, animations: {
                    self.imageView.transform = CGAffineTransform(rotationAngle: .pi)
                    
                })
                UIView.animate(withDuration: 0.5, delay: 0.5, animations: {
                    self.imageView.transform = CGAffineTransform(rotationAngle: .pi * 2)
                    
                })**
                if self.currentSeconds <= 0 {
                    
                    self.stopTimer()
                    AudioServicesPlaySystemSound(1005) 
                }
                
            })
            self.timer?.resume()
        }
        
        
    }
    
    func stopTimer(){
        if self.timerStatus == .pause {
            self.timer?.resume()
        }
        self.timerStatus = .end
        self.cancelButton.isEnabled = false
        UIView.animate(withDuration: 0.5, animations: {
            self.timerLabel.alpha = 0
            self.progessView.alpha = 0
            self.datePicker.alpha = 1
            ***self.imageView.transform = .identity***
        })
        self.toggleButton.isSelected = false
        self.timer?.cancel()
        self.timer = nil
        
        
    }
    
    @IBAction func tapCancelButton(_ sender: UIButton) {
        switch self.timerStatus {
        case .start, .pause:
            self.stopTimer()
        default:
            break
            
        }
    }
    
    @IBAction func tapToggleButton(_ sender: UIButton) {
        self.duration = Int(self.datePicker.countDownDuration)
        self.clockRecord.append(String(self.duration))
        UserDefaults.standard.set(self.clockRecord, forKey: "clockRecord")
        switch self.timerStatus {
        case .end:
            self.currentSeconds = self.duration
            self.timerStatus = .start
            ***UIView.animate(withDuration: 0.5, animations: {
                self.timerLabel.alpha = 1
                self.progessView.alpha = 1
                self.datePicker.alpha = 0
                
            })***
            self.toggleButton.isSelected = true
            self.cancelButton.isEnabled = true
            self.startTimer()
            
        case .start:
            self.timerStatus = .pause
            self.toggleButton.isSelected = false
            self.timer?.suspend()
            
        case .pause:
            self.timerStatus = .start
            self.toggleButton.isSelected =  true
            self.timer?.resume()
    
            
            
        }
    }
    
    
} 

이 부분들, 내가 원하는 방식은 내 앱 메인 화면의 아래에 위치한 꼬맹이 사진이

스타트 버튼을 누르자 마자, (이 경우 datepicker 가 없어지므로) 넓어진 공간을 채우기 위해 크기를 키우는 것이다.

따라서 코드를 이런 식으로 추가해준다.

import UIKit
import AudioToolbox

enum TimerStatus {
    case start
    case pause
    case end
}

class ViewController: UIViewController {

    
    @IBOutlet weak var timerLabel: UILabel!
    @IBOutlet weak var progessView: UIProgressView!
    @IBOutlet weak var datePicker: UIDatePicker!
    
    @IBOutlet weak var cancelButton: UIButton!
    @IBOutlet weak var toggleButton: UIButton!
    
    @IBOutlet weak var imageView: UIImageView!
    
    @IBOutlet weak var StudyView: UIImageView!
    
    
    var duration = 60
    var timerStatus: TimerStatus = .end
    var timer: DispatchSourceTimer?
    var currentSeconds = 0
    
    
    var clockRecord: [String] = []
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        self.configureToggleButton()

  
    }
    

    func setTimerInfoViewVisible(isHidden: Bool){
        self.timerLabel.isHidden = isHidden
        self.progessView.isHidden = isHidden
        
    }
    
    func configureToggleButton(){
        self.toggleButton.setTitle("시작", for: .normal)
        self.toggleButton.setTitle("일시정지", for: .selected)
    }
    
    func startTimer() {
        if self.timer == nil {
            self.timer = DispatchSource.makeTimerSource(flags: [], queue: .main)
            self.timer?.schedule(deadline: .now(), repeating: 1)
            self.timer?.setEventHandler(handler: { [weak self] in
                guard let self = self else { return }
                self.currentSeconds -= 1
                let hour = self.currentSeconds / 3600
                let minute = (self.currentSeconds % 3600) / 60
                let seconds = (self.currentSeconds % 3600) % 60
                self.timerLabel.text = String(format: "%02d:%02d:%02d", hour, minute, seconds)
                self.progessView.progress = Float(self.currentSeconds) / Float(self.duration)
                UIView.animate(withDuration: 0.5, delay: 0, animations: {
                    self.imageView.transform = CGAffineTransform(rotationAngle: .pi)
                    self.StudyView.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
                })
                UIView.animate(withDuration: 0.5, delay: 0.5, animations: {
                    self.imageView.transform = CGAffineTransform(rotationAngle: .pi * 2)
                    
                })
                if self.currentSeconds <= 0 {
                    
                    self.stopTimer()
                    AudioServicesPlaySystemSound(1005) 
                }
                
            })
            self.timer?.resume()
        }
        
        
    }
    
    func stopTimer(){
        if self.timerStatus == .pause {
            self.timer?.resume()
        }
        self.timerStatus = .end
        self.cancelButton.isEnabled = false
        UIView.animate(withDuration: 0.5, animations: {
            self.timerLabel.alpha = 0
            self.progessView.alpha = 0
            self.datePicker.alpha = 1
            self.imageView.transform = .identity
            self.StudyView.transform = .identity
        })
        self.toggleButton.isSelected = false
        self.timer?.cancel()
        self.timer = nil
        
        
    }
    
    @IBAction func tapCancelButton(_ sender: UIButton) {
        switch self.timerStatus {
        case .start, .pause:
            self.stopTimer()
        default:
            break
            
        }
    }
    
    @IBAction func tapToggleButton(_ sender: UIButton) {
        self.duration = Int(self.datePicker.countDownDuration)
        self.clockRecord.append(String(self.duration))
        UserDefaults.standard.set(self.clockRecord, forKey: "clockRecord")
        switch self.timerStatus {
        case .end:
            self.currentSeconds = self.duration
            self.timerStatus = .start
            UIView.animate(withDuration: 0.5, animations: {
                self.timerLabel.alpha = 1
                self.progessView.alpha = 1
                self.datePicker.alpha = 0
                
            })
            self.toggleButton.isSelected = true
            self.cancelButton.isEnabled = true
            self.startTimer()
            
        case .start:
            self.timerStatus = .pause
            self.toggleButton.isSelected = false
            self.timer?.suspend()
            
        case .pause:
            self.timerStatus = .start
            self.toggleButton.isSelected =  true
            self.timer?.resume()
    
            
            
        }
    }
    
    
}

토글 버튼을 누를 때, 멈출 때 새로운 코드가 들어갔고

특히 StartTimer() 함수 핸들러에서 크기가 두 배로 늘어나는 직접적인 함수가 추가된 것이 보일 것이다.

https://user-images.githubusercontent.com/102133961/207801682-db4cda81-bea9-467d-ab6b-cf95e7aa9c25.mov

태그: ,

카테고리:

업데이트:

댓글남기기