SwiftUI tappable subtext

Is there any way in SwiftUI to open browser, when tapping on some part of the text.

I tried the above solution but it doesn’t work because onTapGesture returns View which you cannot add to Text

Text("Some text ").foregroundColor(Color(UIColor.systemGray)) +
Text("clickable subtext")
   .foregroundColor(Color(UIColor.systemBlue))
   .onTapGesture {

   }

I want to have tappable subtext in the main text that’s why using HStack will not work

1

Update for iOS 15 and higher:
There is a new Markdown formatting support for Text, such as:

Text("Some text [clickable subtext](some url) *italic ending* ")

you may check WWDC session with a timecode for details

The old answer for iOS 13 and 14:

Unfortunately there is nothing that resembles NSAttributedString in SwiftUI. And you have only a few options. In this answer you can see how to use UIViewRepresentable for creating an old-school UILabel with click event, for example. But now the only SwiftUI way is to use HStack:

struct TappablePieceOfText: View {
    
    var body: some View {
        
        HStack(spacing: 0) {
            Text("Go to ")
                .foregroundColor(.gray)

            Text("stack overflow")
                .foregroundColor(.blue)
                .underline()
                .onTapGesture {
                    let url = URL.init(string: "/")
                    guard let stackOverflowURL = url, UIApplication.shared.canOpenURL(stackOverflowURL) else { return }
                    UIApplication.shared.open(stackOverflowURL)
                }
            
            Text(" and enjoy")
                .foregroundColor(.gray)
        }
        
        
    }
}

UPDATE
Added solution with UITextView and UIViewRepresentable. I combined everything from added links and the result is quite good, I think:

import SwiftUI
import UIKit

struct TappablePieceOfText: View {
    
    var body: some View {
        TextLabelWithHyperlink()
            .frame(width: 300, height: 110)
    }
    
}

struct TextLabelWithHyperlink: UIViewRepresentable {
    
    func makeUIView(context: Context) -> UITextView {
        
        let standartTextAttributes: [NSAttributedString.Key : Any] = [
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
            NSAttributedString.Key.foregroundColor: UIColor.gray
        ]
        
        let attributedText = NSMutableAttributedString(string: "You can go to ")
        attributedText.addAttributes(standartTextAttributes, range: attributedText.range) // check extention
        
        let hyperlinkTextAttributes: [NSAttributedString.Key : Any] = [
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
            NSAttributedString.Key.foregroundColor: UIColor.blue,
            NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
            NSAttributedString.Key.link: "https://stackoverflow.com"
        ]
        
        let textWithHyperlink = NSMutableAttributedString(string: "stack overflow site")
        textWithHyperlink.addAttributes(hyperlinkTextAttributes, range: textWithHyperlink.range)
        attributedText.append(textWithHyperlink)
        
        let endOfAttrString = NSMutableAttributedString(string: " end enjoy it using old-school UITextView and UIViewRepresentable")
        endOfAttrString.addAttributes(standartTextAttributes, range: endOfAttrString.range)
        attributedText.append(endOfAttrString)
        
        let textView = UITextView()
        textView.attributedText = attributedText
        
        textView.isEditable = false
        textView.textAlignment = .center
        textView.isSelectable = true
        
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {}
    
}

result of HStack and Text:

result of UIViewRepresentable and UITextView:

UPDATE 2:
here is a NSMutableAttributedString little extension:

extension NSMutableAttributedString {
    
    var range: NSRange {
        NSRange(location: 0, length: self.length)
    }
    
}

7

I didn’t have the patience to make the UITextView and UIViewRepresentable work, so instead I made the whole paragraph tappable but still kept the underscored URL look/feel. Especially helpful if you are trying to add Terms of Service URL link to your app.

The code is fairly simple:

Button(action: {
    let tosURL = URL.init(string: "https://www.google.com")! // add your link here
    if UIApplication.shared.canOpenURL(tosURL) {
        UIApplication.shared.open(tosURL)
    }
}, label: {
    (Text("Store.ly helps you find storage units nearby. By continuing, you agree to our ")
        + Text("Terms of Service.")
            .underline()
        )
        .frame(maxWidth: .infinity, alignment: .leading)
        .font(Font.system(size: 14, weight: .medium))
        .foregroundColor(Color.black)
        .fixedSize(horizontal: false, vertical: true)
})
    .padding([.horizontal], 20)

2

Starting from iOS 15 you can use AttributedString and Markdown with Text.

An example of using Markdown:

Text("Plain text. [This is a tappable link](https://stackoverflow.com)")

AttributedString gives you more control over formatting. For example, you can change a link color:

var string = AttributedString("Plain text. ")
        
var tappableText = AttributedString("I am tappable!")
tappableText.link = URL(string: "https://stackoverflow.com")
tappableText.foregroundColor = .green

string.append(tappableText)

Text(string)

Here is what it looks like:

A side note: if you want your tappable text to have a different behavior from opening a URL in a browser, you can define a custom URL scheme for your app. Then you will be able to handle tap events on a link using onOpenURL(perform:) that registers a handler to invoke when the view receives a url for the scene or window the view is in.

0

Improved version of @alexander-poleschuk for iOS 15

struct ContentView: View {

let string: AttributedString

init() {
    var string = AttributedString("Plain text. ")
    var tappableText = AttributedString("I am tappable!")
    //You can use any URL
    tappableText.link = URL(string: "application://")
    tappableText.foregroundColor = .green

    string.append(tappableText)

    self.string = string

}

var body: some View {
    Text(string)
        .environment(.openURL, OpenURLAction { url in
            print("Hello")
            return .discarded
        })
  }
}

1

Base on Dhaval Bera’s code, I put some struct.

struct TextLabelWithHyperLink: UIViewRepresentable {
  
  @State var tintColor: UIColor
  
  @State var hyperLinkItems: Set<HyperLinkItem>
  
  private var _attributedString: NSMutableAttributedString
  
  private var openLink: (HyperLinkItem) -> Void
  
  init (
    tintColor: UIColor,
    string: String,
    attributes: [NSAttributedString.Key : Any],
    hyperLinkItems: Set<HyperLinkItem>,
    openLink: @escaping (HyperLinkItem) -> Void
  ) {
    self.tintColor = tintColor
    self.hyperLinkItems = hyperLinkItems
    self._attributedString = NSMutableAttributedString(
      string: string,
      attributes: attributes
    )
    self.openLink = openLink
  }
  
  
  func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.isEditable = false
    textView.isSelectable = true
    textView.tintColor = self.tintColor
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    return textView
  }
  
  func updateUIView(_ uiView: UITextView, context: Context) {
   
    for item in hyperLinkItems {
      let subText = item.subText
      let link = item.subText.replacingOccurrences(of: " ", with: "_")
      
      _attributedString
        .addAttribute(
          .link,
          value: String(format: "https://%@", link),
          range: (_attributedString.string as NSString).range(of: subText)
        )
    }
    
    uiView.attributedText = _attributedString
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
  }
  
  class Coordinator: NSObject, UITextViewDelegate {
    var parent : TextLabelWithHyperLink
    
    init( parent: TextLabelWithHyperLink ) {
      self.parent = parent
    }
    
    func textView(
      _ textView: UITextView,
      shouldInteractWith URL: URL,
      in characterRange: NSRange,
      interaction: UITextItemInteraction
    ) -> Bool {
      
      let strPlain = URL.absoluteString
        .replacingOccurrences(of: "https://", with: "")
        .replacingOccurrences(of: "_", with: " ")
      
      if let ret = parent.hyperLinkItems.first(where: { $0.subText == strPlain }) {
        parent.openLink(ret)
      }
      
      return false
    }
  }
}

struct HyperLinkItem: Hashable {
    
  let subText : String
  let attributes : [NSAttributedString.Key : Any]?
  
  init (
    subText: String,
    attributes: [NSAttributedString.Key : Any]? = nil
  ) {
    self.subText = subText
    self.attributes = attributes
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(subText)
  }
    
  static func == (lhs: HyperLinkItem, rhs: HyperLinkItem) -> Bool {
    lhs.hashValue == rhs.hashValue
  }
}

Usage:


TextLabelWithHyperLink(
  tintColor: .green,
  string: "Please contact us by filling contact form. We will contact with you shortly.  Your request will be processed in accordance with the Terms of Use and Privacy Policy.",
  attributes: [:],
  hyperLinkItems: [
    .init(subText: "processed"),
    .init(subText: "Terms of Use"),
  ],
  openLink: {
  (tappedItem) in
    print("Tapped link: (tappedItem.subText)")
  }
)

0

Tappable String using UITextView

struct TextLabelWithHyperlink: UIViewRepresentable {

@State var tintColor: UIColor = UIColor.black
@State var arrTapableString: [String] = []

var configuration = { (view: UITextView) in }
var openlink = {(strtext: String) in}

func makeUIView(context: Context) -> UITextView {
    
    let textView = UITextView()
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.isEditable = false
    textView.isSelectable = true
    textView.tintColor = self.tintColor
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    return textView
}

func updateUIView(_ uiView: UITextView, context: Context) {
    configuration(uiView)
    let stringarr  = NSMutableAttributedString(attributedString: uiView.attributedText)
    for strlink in arrTapableString{
        let link = strlink.replacingOccurrences(of: " ", with: "_")
        stringarr.addAttribute(.link, value: String(format: "https://%@", link), range: (stringarr.string as NSString).range(of: strlink))
    }
    uiView.attributedText = stringarr
}

func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
}

class Coordinator: NSObject,UITextViewDelegate {
    var parent : TextLabelWithHyperlink
    init(parent: TextLabelWithHyperlink) {
        self.parent = parent
    }
    
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        let strPlain = URL.absoluteString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "_", with: " ")
        if (self.parent.arrTapableString.contains(strPlain)) {
            self.parent.openlink(strPlain)
        }
        return false
    }
    
}}

Implementation in swiftui

TextLabelWithHyperlink(arrTapableString: ["Terms of Use", "Privacy Policy"]) { (textView) in
                            let string = "Please contact us by filling contact form. We will contact with you shortly.  Your request will be processed in accordance with the Terms of Use and Privacy Policy."
                            
                            let attrib = NSMutableAttributedString(string: string, attributes: [.font: UIFont(name: Poodlife_Font.oxygen_regular, size: 14)!,.foregroundColor:  UIColor.black])
                            
                            attrib.addAttributes([.font: UIFont(name: Font.oxygen_bold, size: 14)!,
                                                  .foregroundColor:  UIColor.black], range: (string as NSString).range(of: "Terms of Use"))
                            
                            attrib.addAttributes([.font: UIFont(name: Font.oxygen_bold, size: 14)!,
                                                  .foregroundColor:  UIColor.black,
                                                  .link: "Privacy_Policy"], range: (string as NSString).range(of: "Privacy Policy"))
                            
                            textView.attributedText = attrib
                        } openlink: { (tappedString) in
                            print("Tapped link:(tappedString)")
                        }

Below is my fully SwiftUI solution. With the below solution, any container you put this in will nicely be formatted and you can make the specific text you want clickable.

struct TermsAndPrivacyText: View {
  @State private var sheet: TermsOrPrivacySheet? = nil
  let string = "By signing up, you agree to XXXX's Terms & Conditions and Privacy Policy"
  
  
  enum TermsOrPrivacySheet: Identifiable {
    case terms, privacy
    
    var id: Int {
      hashValue
    }
  }
  
  
  func showSheet(_ string: String) {
    if ["Terms", "&", "Conditions"].contains(string) {
      sheet = .terms
    }
    else if ["Privacy", "Policy"].contains(string) {
      sheet = .privacy
    }
  }
  
  
  func fontWeight(_ string: String) -> Font.Weight {
    ["Terms", "&", "Conditions", "Privacy", "Policy"].contains(string) ? .medium : .light
  }

  
  private func createText(maxWidth: CGFloat) -> some View {
    var width = CGFloat.zero
    var height = CGFloat.zero
    let stringArray = string.components(separatedBy: " ")
    
    
    return
      ZStack(alignment: .topLeading) {
        ForEach(stringArray, id: .self) { string in
          Text(string + " ")
            .font(Theme.Fonts.ofSize(14))
            .fontWeight(fontWeight(string))
            .onTapGesture { showSheet(string) }
            .alignmentGuide(.leading, computeValue: { dimension in
              if (abs(width - dimension.width) > maxWidth) {
                width = 0
                height -= dimension.height
              }
              
              let result = width
              if string == stringArray.last {
                width = 0
              }
              else {
                width -= dimension.width
               }
              
              return result
            })
            .alignmentGuide(.top, computeValue: { dimension in
              let result = height
              if string == stringArray.last { height = 0 }
              return result
            })
          }
      }
      .frame(maxWidth: .infinity, alignment: .topLeading)
    }
  
  
  var body: some View {
      GeometryReader { geo in
        ZStack {
          createText(maxWidth: geo.size.width)
        }
      }
      .frame(maxWidth: .infinity)
      .sheet(item: $sheet) { item in
        switch item {
        case .terms:
          TermsAndConditions()
        case .privacy:
          PrivacyPolicy()
        }
      }
  }
}

0

For iOS 14

I used a third party library like Down. It’s a lot simpler than creating your own parsing engine.

import SwiftUI
import Down

struct ContentView: View {
    @State var source = NSAttributedString()

    var body: some View {
        VStack {
            TextView(attributedText: source)
                .padding(.horizontal)
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity, minHeight: 64, maxHeight: 80, alignment: .leading)
                .background(Color( red: 236/255, green: 236/255, blue: 248/255))
                .cornerRadius(10)
                .padding()
        }
        .onAppear {
            let down = Down(markdownString: "Work hard to get what you like, otherwise you'll be forced to just like what you get! [tap here](https://apple.com)")
            source = try! down.toAttributedString(.default, stylesheet: "* {font-family: 'Avenir Black'; font-size: 15}")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct TextView: UIViewRepresentable {
    var attributedText: NSAttributedString

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isEditable = false
        textView.backgroundColor = .clear
        textView.isUserInteractionEnabled = true
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = attributedText
    }
}

enter image description here

I used @АлександрГрабовский answer, but I also had to do some configs to make it work for me.
I have 2 links in my text field, both of them have a custom colour and directs the user to different pages.
I also didn’t want the scroll to be enabled, but if I disabled it the height wouldn’t get adjusted and it would stretch to the outside of the view.
I tried SO MANY different things and I found, for the moment, a solution that works for me, so I thought I might as well share it here.

Again, thanks to @АлександрГрабовский answer I managed to do it. The only tweaks I had to do were:

  1. set the links attributes related to the text colour to another var and set the “linkTextAttributes” property on the UITextView to that, in order to change the text colour, while the font and link destination I used what was suggested in his response. The text colour didn’t change if I set the colour attributes to the link itself.

    let linkAttributes: [NSAttributedString.Key : Any] = [
    NSAttributedString.Key.foregroundColor: UIColor(named: “my_custom_green”) ?? UIColor.blue
    ]
    textView.linkTextAttributes = linkAttributes

  2. I didn’t want the UITextView to scroll and the only way I found to keep the multi line height and not scroll (setting isScrollEnabled to false didn’t work for me) was to set scrollRangeToVisible to the last string range I had.

    textView.scrollRangeToVisible(ppWithHyperlink.range)

I don’t know if this is the best alternative, but it is what I found… hope in the future there’s a better way to do this in swiftUI!!!

In Ios 15 you can just try

Text("Apple website: [click here](https://apple.com)")

I wanted to use single string with MarkDown and also wanted to be able to provide any attribute i want to my links. Because adding tintColor only without underlining wasn’t enough.

So i had to come up with a solution based on converting MarkDown to AttributedString to be able to provide any attribute to links and i wanted to be %100 SwiftUI. Something like these:

The idea is basically finding links in MarkDown text and replacing them with their link texts and store their ranges. And at the end, add attributes to those ranges inside AttributedString.

This is the helper that i created:

enum MarkDownMapper {

    static func map(_ text: String) -> MarkDownModel {
        var attributedText = AttributedString(text)
        var components = [MarkDownModel.Component]()

        let linkRanges = getLinkRanges(in: attributedText)
        linkRanges.forEach { _ in
            // Check every time in the loop to get updated value since text is mutable.
            if let firstLinkRange = getLinkRanges(in: attributedText).first,
               let url = extractURL(from: attributedText),
               let urlText = extractURLText(from: attributedText)
            {
                // Replace whole mark down link with the urlText.
                attributedText.replaceSubrange(firstLinkRange, with: AttributedString(urlText))
                let upperRange = attributedText.index(
                    firstLinkRange.lowerBound,
                    offsetByCharacters: urlText.count
                )
                components.append(
                    MarkDownModel.Component(
                        text: urlText,
                        url: url,
                        range: firstLinkRange.lowerBound ..< upperRange
                    )
                )
            }
        }
        return MarkDownModel(text: attributedText, components: components)
    }

    static private func getLinkRanges(
        in attributedString: AttributedString
    ) -> [Range<AttributedString.Index>] {
        var ranges = [Range<AttributedString.Index>]()
        let text = NSAttributedString(attributedString).string

        // Define the pattern for markdown links
        let pattern = #"[[^]]+]((https?://[^)]+))"#
        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
            return ranges
        }

        let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
        for match in matches {
            if let range = Range(match.range, in: text),
               let attributedRange = attributedString.range(of: text[range]) {
                ranges.append(attributedRange)
            }
        }

        return ranges
    }

    static private func extractURL(from text: AttributedString) -> URL? {
        let urlString = extract(from: text, with: "\(([^)]+)\)") // URL pattern between parentheses.
        return URL(string: urlString ?? "")
    }

    static private func extractURLText(from text: AttributedString) -> String? {
        extract(from: text, with: "\[([^]]+)\]") // URL name pattern between brackets.
    }

    static private func extract(from text: AttributedString, with regexPattern: String) -> String? {
        let text = NSAttributedString(text).string
        let textRange = NSRange(location: 0, length: text.utf16.count)
        guard let regex = try? NSRegularExpression(pattern: regexPattern, options: []),
              let match = regex.firstMatch(in: text, options: [], range: textRange),
              let innerTextRange = Range(match.range(at: 1), in: text)
        else { return nil }
        return String(text[innerTextRange])
    }
}

struct MarkDownModel {

    let text: AttributedString
    let components: [Component]

    struct Component {
        let text: String
        let url: URL
        let range: Range<AttributedString.Index>
    }
}

And then we can use it like this:

struct TappableText: View {

    private let urls: [URL]
    let attributedString: AttributedString
    let onTap: (Int, URL) -> Void

    init(
        text: String,
        onTap: @escaping (Int, URL) -> Void
    ) {
        let model = MarkDownMapper.map(text)
        var attributedString = model.text
        model.components.forEach { component in
            attributedString[component.range].link = component.url
            attributedString[component.range].foregroundColor = .orange // Change and add whatever you want
            attributedString[component.range].strikethroughStyle = .single // Change and add whatever you want
        }
        self.urls = model.components.map { $0.url }
        self.attributedString = attributedString
        self.onTap = onTap
    }

    var body: some View {
        Text(attributedString)
            .environment(.openURL, OpenURLAction { url in
                if let index = urls.firstIndex(where: { $0 == url }) {
                    onTap(index, url)
                }
                return .handled
            })
    }
}

And as a bonus these are the unit tests i added if anyone is interested:

final class MarkDownMapperTests: XCTestCase {

    func testMap_withSingleLink() {
        let input = "This is a [link](https://example.com)"
        let expectedURL = URL(string: "https://example.com")!
        let expectedText = AttributedString("This is a link")
        let expectedRange = expectedText.createIndexRange(10, 14)

        let result = MarkDownMapper.map(input)

        XCTAssertEqual(result.text, expectedText)
        XCTAssertEqual(result.components.count, 1)
        XCTAssertEqual(result.components.first?.url, expectedURL)
        XCTAssertEqual(result.components.first?.range, expectedRange)
    }

    func testMap_withMultipleLinks() {
        let input = """
        This is a [link1](https://example1.com) and this is a [link2](https://example2.com).
        Here's another [link33](https://example3.com) with more [link4](https://example4.com) links.
        And the last [link556](https://example5.com).
        """
        let expectedText = AttributedString("""
        This is a link1 and this is a link2.
        Here's another link33 with more link4 links.
        And the last link556.
        """)

        let result = MarkDownMapper.map(input)

        XCTAssertEqual(result.text, expectedText)
        XCTAssertEqual(result.components.count, 5)

        let expectedComponents: [(String, String, Range<AttributedString.Index>)] = [
            ("link1", "https://example1.com", expectedText.createIndexRange(10, 15)),
            ("link2", "https://example2.com", expectedText.createIndexRange(30, 35)),
            ("link33", "https://example3.com", expectedText.createIndexRange(52, 58)),
            ("link4", "https://example4.com", expectedText.createIndexRange(69, 74)),
            ("link556", "https://example5.com", expectedText.createIndexRange(95, 102)),
        ]

        for (index, component) in result.components.enumerated() {
            print(index, result.components.count, expectedComponents.count)
            XCTAssertEqual(component.text, expectedComponents[index].0)
            XCTAssertEqual(component.url, URL(string: expectedComponents[index].1))
            XCTAssertEqual(component.range, expectedComponents[index].2)
        }
    }

    func testMap_withNonURLValues() {
        let input = "This text has no links but has [non-link text](non-link value)"
        let expectedText = "This text has no links but has [non-link text](non-link value)"

        let result = MarkDownMapper.map(input)

        XCTAssertEqual(result.text, AttributedString(expectedText))
        XCTAssertEqual(result.components.count, 0)
        XCTAssertNil(result.components.first?.url)
    }

    func testMap_withNoLinks() {
        let input = "This text has no links."
        let expectedText = "This text has no links."

        let result = MarkDownMapper.map(input)

        XCTAssertEqual(result.text, AttributedString(expectedText))
        XCTAssertEqual(result.components.count, 0)
    }
}

private extension AttributedString {

    func createIndexRange(_ lowerRange: Int, _ upperRange: Int) -> Range<AttributedString.Index> {
        index(startIndex, offsetByCharacters: lowerRange)..<index(startIndex, offsetByCharacters: upperRange)
    }
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật