├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── ExpandableText
│ ├── ExpandableText.swift
│ └── Extensions.swift
└── Tests
└── ExpandableTextTests
└── ExpandableTextTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 NuPlay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ExpandableText",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "ExpandableText",
15 | targets: ["ExpandableText"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "ExpandableText",
26 | dependencies: []),
27 | .testTarget(
28 | name: "ExpandableTextTests",
29 | dependencies: ["ExpandableText"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
ExpandableText 😎 (SwiftUI)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Expand the text with the "more" button
16 |
17 |
18 |
19 |
20 | ## Get Started
21 |
22 | ```swift
23 | import SwiftUI
24 | import ExpandableText
25 |
26 | struct ExpandableText_Test: View {
27 |
28 | @State private var sampleText: String = "Do you think you're living an ordinary life? You are so mistaken it's difficult to even explain. The mere fact that you exist makes you extraordinary. The odds of you existing are less than winning the lottery, but here you are. Are you going to let this extraordinary opportunity pass?"
29 |
30 | var body: some View {
31 | ExpandableText(text: sampleText)
32 | .font(.body)//optional
33 | .foregroundColor(.primary)//optional
34 | .lineLimit(3)//optional
35 | .expandButton(TextSet(text: "more", font: .body, color: .blue))//optional
36 | .collapseButton(TextSet(text: "less", font: .body, color: .blue))//optional
37 | .expandAnimation(.easeOut)//optional
38 | .padding(.horizontal, 24)//optional
39 | }
40 | }
41 | ```
42 |
43 | ## Modifier(optional)
44 |
45 | Modifier | Default
46 | --- | ---
47 | `.font(_ font: Font)` | `.body`
48 | `.lineLimit(_ lineLimit: Int)` | `3`
49 | `.foregroundColor(_ color: Color)` | `.primary`
50 | `.expandButton(_ expandButton: TextSet)` | `TextSet(text: "more", font: .body, color: .blue)`
51 | `.collapseButton(_ collapseButton: TextSet?)` | `nil(If it's nil, it doesn't show)`
52 | `.expandAnimation(_ animation: Animation?)` | `.none`
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Sources/ExpandableText/ExpandableText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExpandableText.swift
3 | //
4 | //
5 | // Created by 이웅재 on 2021/10/12.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct ExpandableText: View {
11 | var text : String
12 |
13 | @available(iOS 15, *)
14 | var markdownText: AttributedString {
15 | (try? AttributedString(markdown: text, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? AttributedString()
16 | }
17 |
18 | var font: Font = .body
19 | var lineLimit: Int = 3
20 | var foregroundColor: Color = .primary
21 |
22 | var expandButton: TextSet = TextSet(text: "more", font: .body, color: .blue)
23 | var collapseButton: TextSet? = nil
24 |
25 | var animation: Animation? = .none
26 |
27 | @State private var expand : Bool = false
28 | @State private var truncated : Bool = false
29 | @State private var fullSize: CGFloat = 0
30 |
31 | public init(text: String) {
32 | self.text = text
33 | }
34 | public var body: some View {
35 | ZStack(alignment: .bottomTrailing){
36 | Group {
37 | if #available(iOS 15.0, *) {
38 | Text(markdownText)
39 | } else {
40 | Text(text)
41 | }
42 | }
43 | .font(font)
44 | .foregroundColor(foregroundColor)
45 | .lineLimit(expand == true ? nil : lineLimit)
46 | .animation(animation, value: expand)
47 | .mask(
48 | VStack(spacing: 0){
49 | Rectangle()
50 | .foregroundColor(.black)
51 |
52 | HStack(spacing: 0){
53 | Rectangle()
54 | .foregroundColor(.black)
55 | if truncated{
56 | if !expand {
57 | HStack(alignment: .bottom,spacing: 0){
58 | LinearGradient(
59 | gradient: Gradient(stops: [
60 | Gradient.Stop(color: .black, location: 0),
61 | Gradient.Stop(color: .clear, location: 0.8)]),
62 | startPoint: .leading,
63 | endPoint: .trailing)
64 | .frame(width: 32, height: expandButton.text.heightOfString(usingFont: fontToUIFont(font: expandButton.font)))
65 |
66 | Rectangle()
67 | .foregroundColor(.clear)
68 | .frame(width: expandButton.text.widthOfString(usingFont: fontToUIFont(font: expandButton.font)), alignment: .center)
69 | }
70 | }
71 | else if let collapseButton = collapseButton {
72 | HStack(alignment: .bottom,spacing: 0){
73 | LinearGradient(
74 | gradient: Gradient(stops: [
75 | Gradient.Stop(color: .black, location: 0),
76 | Gradient.Stop(color: .clear, location: 0.8)]),
77 | startPoint: .leading,
78 | endPoint: .trailing)
79 | .frame(width: 32, height: collapseButton.text.heightOfString(usingFont: fontToUIFont(font: collapseButton.font)))
80 |
81 | Rectangle()
82 | .foregroundColor(.clear)
83 | .frame(width: collapseButton.text.widthOfString(usingFont: fontToUIFont(font: collapseButton.font)), alignment: .center)
84 | }
85 | }
86 | }
87 | }
88 | .frame(height: expandButton.text.heightOfString(usingFont: fontToUIFont(font: font)))
89 | }
90 | )
91 |
92 | if truncated {
93 | if let collapseButton = collapseButton {
94 | Button(action: {
95 | self.expand.toggle()
96 | }, label: {
97 | Text(expand == false ? expandButton.text : collapseButton.text)
98 | .font(expand == false ? expandButton.font : collapseButton.font)
99 | .foregroundColor(expand == false ? expandButton.color : collapseButton.color)
100 | })
101 | }
102 | else if !expand {
103 | Button(action: {
104 | self.expand = true
105 | }, label: {
106 | Text(expandButton.text)
107 | .font(expandButton.font)
108 | .foregroundColor(expandButton.color)
109 | })
110 | }
111 | }
112 | }
113 | .background(
114 | ZStack{
115 | if !truncated {
116 | if fullSize != 0 {
117 | Text(text)
118 | .font(font)
119 | .lineLimit(lineLimit)
120 | .background(
121 | GeometryReader { geo in
122 | Color.clear
123 | .onAppear {
124 | if fullSize > geo.size.height {
125 | self.truncated = true
126 | print(geo.size.height)
127 | }
128 | }
129 | }
130 | )
131 | }
132 |
133 | Text(text)
134 | .font(font)
135 | .lineLimit(999)
136 | .fixedSize(horizontal: false, vertical: true)
137 | .background(GeometryReader { geo in
138 | Color.clear
139 | .onAppear() {
140 | self.fullSize = geo.size.height
141 | }
142 | })
143 | }
144 | }
145 | .hidden()
146 | )
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Sources/ExpandableText/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | //
4 | //
5 | // Created by 이웅재 on 2021/10/12.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension ExpandableText {
11 | public func font(_ font: Font) -> ExpandableText {
12 | var result = self
13 |
14 | result.font = font
15 |
16 | return result
17 | }
18 | public func lineLimit(_ lineLimit: Int) -> ExpandableText {
19 | var result = self
20 |
21 | result.lineLimit = lineLimit
22 | return result
23 | }
24 |
25 | public func foregroundColor(_ color: Color) -> ExpandableText {
26 | var result = self
27 |
28 | result.foregroundColor = color
29 | return result
30 | }
31 |
32 | public func expandButton(_ expandButton: TextSet) -> ExpandableText {
33 | var result = self
34 |
35 | result.expandButton = expandButton
36 | return result
37 | }
38 |
39 | public func collapseButton(_ collapseButton: TextSet) -> ExpandableText {
40 | var result = self
41 |
42 | result.collapseButton = collapseButton
43 | return result
44 | }
45 |
46 | public func expandAnimation(_ animation: Animation?) -> ExpandableText {
47 | var result = self
48 |
49 | result.animation = animation
50 | return result
51 | }
52 | }
53 |
54 | extension String {
55 | func heightOfString(usingFont font: UIFont) -> CGFloat {
56 | let fontAttributes = [NSAttributedString.Key.font: font]
57 | let size = self.size(withAttributes: fontAttributes)
58 | return size.height
59 | }
60 |
61 | func widthOfString(usingFont font: UIFont) -> CGFloat {
62 | let fontAttributes = [NSAttributedString.Key.font: font]
63 | let size = self.size(withAttributes: fontAttributes)
64 | return size.width
65 | }
66 | }
67 |
68 | public struct TextSet {
69 | var text: String
70 | var font: Font
71 | var color: Color
72 |
73 | public init(text: String, font: Font, color: Color) {
74 | self.text = text
75 | self.font = font
76 | self.color = color
77 | }
78 | }
79 |
80 | func fontToUIFont(font: Font) -> UIFont {
81 | if #available(iOS 14.0, *) {
82 | switch font {
83 | case .largeTitle:
84 | return UIFont.preferredFont(forTextStyle: .largeTitle)
85 | case .title:
86 | return UIFont.preferredFont(forTextStyle: .title1)
87 | case .title2:
88 | return UIFont.preferredFont(forTextStyle: .title2)
89 | case .title3:
90 | return UIFont.preferredFont(forTextStyle: .title3)
91 | case .headline:
92 | return UIFont.preferredFont(forTextStyle: .headline)
93 | case .subheadline:
94 | return UIFont.preferredFont(forTextStyle: .subheadline)
95 | case .callout:
96 | return UIFont.preferredFont(forTextStyle: .callout)
97 | case .caption:
98 | return UIFont.preferredFont(forTextStyle: .caption1)
99 | case .caption2:
100 | return UIFont.preferredFont(forTextStyle: .caption2)
101 | case .footnote:
102 | return UIFont.preferredFont(forTextStyle: .footnote)
103 | case .body:
104 | return UIFont.preferredFont(forTextStyle: .body)
105 | default:
106 | return UIFont.preferredFont(forTextStyle: .body)
107 | }
108 | } else {
109 | switch font {
110 | case .largeTitle:
111 | return UIFont.preferredFont(forTextStyle: .largeTitle)
112 | case .title:
113 | return UIFont.preferredFont(forTextStyle: .title1)
114 | // case .title2:
115 | // return UIFont.preferredFont(forTextStyle: .title2)
116 | // case .title3:
117 | // return UIFont.preferredFont(forTextStyle: .title3)
118 | case .headline:
119 | return UIFont.preferredFont(forTextStyle: .headline)
120 | case .subheadline:
121 | return UIFont.preferredFont(forTextStyle: .subheadline)
122 | case .callout:
123 | return UIFont.preferredFont(forTextStyle: .callout)
124 | case .caption:
125 | return UIFont.preferredFont(forTextStyle: .caption1)
126 | // case .caption2:
127 | // return UIFont.preferredFont(forTextStyle: .caption2)
128 | case .footnote:
129 | return UIFont.preferredFont(forTextStyle: .footnote)
130 | case .body:
131 | return UIFont.preferredFont(forTextStyle: .body)
132 | default:
133 | return UIFont.preferredFont(forTextStyle: .body)
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Tests/ExpandableTextTests/ExpandableTextTests.swift:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------